[
  {
    "path": ".gitattributes",
    "content": "# Auto detect text files and perform LF normalization\n* text=auto\n"
  },
  {
    "path": ".gitignore",
    "content": ".gradle\nbuild/\n!gradle/wrapper/gradle-wrapper.jar\n!**/src/main/**/build/\n!**/src/test/**/build/\nbin/\nconfig/bin/\ndomain/bin/\ni18n/bin/\nimageprocess/bin/\nopencv/bin/\n\n### IntelliJ IDEA ###\n.idea/\n*.iws\n*.iml\n*.ipr\nout/\n!**/src/main/**/out/\n!**/src/test/**/out/\n\n### VS Code ###\n.vscode/\n\n### Mac OS ###\n.DS_Store\n\n.idea\n.kotlin\n\n# cache\nrxcache/\n\n# log\nlog/\n\n# spec\n.cursor/\n.specify"
  },
  {
    "path": "CHANGELOG.md",
    "content": "Monica\n===\n\nVersion 1.1.4\n---\n2025-9-25\n* 增加使用 Gemini 实现自然语言实现调色的功能\n* 增加主题的功能，Monica 可以切换主题\n* 增加国际化，支持英文版本\n* 重构 Monica UI 的首页\n* 重构图像绘制形状模块\n* 重构涂鸦模块\n\nVersion 1.1.3\n---\n2025-8-4\n* 增加使用自然语言实现调色的功能\n* 增加 OpenCV 调参过程的记录\n\nVersion 1.1.2\n---\n2025-7-28\n* 优化图像调色的代码（使用图像金字塔优化raw、heif文件的加载和调色）\n\nVersion 1.1.1\n---\n2025-7-15\n* 优化 jni 的代码\n* 优化图像调色的代码（使用并行 + LUT）\n\nVersion 1.1.0\n---\n2025-6-25\n* 在 macOS 上从零构建 libheif + OpenCV 图像处理库，而不是依赖 brew 安装的库\n* 优化代码\n\nVersion 1.0.9\n---\n2025-6-6\n* 增加多种格式的导入导出\n* 修复 macOS 打包安装失败的 bug\n\nVersion 1.0.8\n---\n2025-4-20\n* 修复保存 png 图像出错的 bug\n\nVersion 1.0.7\n---\n2025-4-18\n* 优化图片的加载过程\n* 支持使用 GPU 来推理(前提是需要支持CUDA)\n* 增加生成多种风格的漫画\n\nVersion 1.0.6\n---\n2025-4-8\n* 滤镜数量增加到50多款\n* 模型的调用从调用本地算法迁移到通过 http 调用算法服务，减少软件对模型文件的依赖。\n* 增加软件的通用设置\n* Kotlin 版本升级到 2.1.0\n\nVersion 1.0.5\n---\n2025-3-6\n* 增加将多张图片生成 gif 的功能\n* 优化滤镜相关的配置\n\nVersion 1.0.4\n---\n2025-1-23\n* 完善对 CV 算法快速调参的模块\n\nVersion 1.0.3\n---\n2024-11-27\n* 修复图像调色的 bug\n\nVersion 1.0.2\n---\n2024-11-26\n* 增加形状绘制和添加文字\n* 增加图像调色\n* 完善 Monica 常用的组件\n\nVersion 1.0.1\n---\n2024-11-3\n* 增加对 CV 算法快速调参的模块\n* 完善 Monica 常用的组件\n\nVersion 1.0.0\n---\n2024-9-18\n* Kotlin 版本升到 2.0.20\n* 增加图像错切\n* 增加多种图像增强的算法\n* 增加深度学习相关的功能（人脸检测、生成素描画、替换人脸）\n\nVersion 0.2.6\n---\n2024-7-18\n* 在 MacOS(只针对Intel 芯片）增加多种图像增强的算法，通过 OpenCV C++ 实现\n\nVersion 0.2.5\n---\n2024-7-13\n* 增加 NatureFilter 滤镜\n* 增加 logback 作为日志框架\n\nVersion 0.2.4\n---\n2024-6-30\n* 增加 FastBlur2D 滤镜\n* 增加图像裁剪的属性\n\nVersion 0.2.3\n---\n2024-6-17\n* 增加图片取色功能\n* 增加 ColorFilter\n* 优化架构\n\nVersion 0.2.2\n---\n2024-6-13\n* 完善图像的裁剪功能\n\nVersion 0.2.1\n---\n2024-6-9\n* 优化裁剪的 UI\n* 优化滤镜相关的架构\n\nVersion 0.2.0\n---\n2024-5-29\n* 增加图像的裁剪功能\n* 增加 VignetteFilter\n* 增加 toast 提示\n\nVersion 0.1.5\n---\n2024-5-25\n* 升级 koin 版本\n* 优化图像的涂鸦功能\n* 增加 StrokeAreaFilter\n\nVersion 0.1.4\n---\n2024-5-24\n* 增加图像的涂鸦功能\n\nVersion 0.1.3\n---\n2024-5-11\n* 增加图像的 resize 功能\n* 增加带 tooltip 的按钮\n\nVersion 0.1.2\n---\n2024-5-9\n* 增加 EmbossFilter、OilPaintFilter\n* 修复某种情况下无法保存图像的 bug\n\nVersion 0.1.1\n---\n2024-5-8\n* 增加图像的 flip、rotate 功能\n* 引入 koin 作为依赖注入的容器\n\nVersion 0.1.0\n---\n2024-5-6\n* 提供加载本地图片、网络图片。\n* 对图片局部模糊、打马赛克。\n* 调整图片的饱和度、色相、亮度。\n* 提供 20 款滤镜，大多数滤镜也可以单独调整参数。\n* 对修改的图像进行保存。\n* 放大、缩小图像。"
  },
  {
    "path": "FUNCTION.md",
    "content": "## 2.1 基础功能\n加载完图像后，就可以对图像进行各种编辑和操作\n![](images/1-1.png)\n\nMonica 基础功能的按钮，都带有 tooltips ，例如这个涂鸦功能\n![](images/1-2.png)\n\n点击按钮就可以进入涂鸦界面，对图像进行随意的涂鸦。\n![](images/1-3.png)\n\n由于 Monica 是一款桌面软件，画笔由鼠标进行控制。画笔默认是黑色的，可以随着鼠标的移动而进行绘制曲线。Monica 支持选择画笔的颜色，以及选择画笔的粗细。\n![](images/1-4.png)\n![](images/1-5.png)\n\n涂鸦完之后，记得保存图片，这样回到主界面之后才真正的保存结果了。\n![](images/1-6.png)\n\n在基础功能里，还有一个比较有意思的功能，对图像取色\n![](images/1-7.png)\n\n这个功能通过点击图像中的位置，获取颜色相关的信息，包括 HEX 颜色代码值、RGB 值、HSL 值和 HSV 值。\n![](images/1-8.png)\n![](images/1-9.png)\n\n## 2.2 裁剪\n基础功能有个比较强大的功能——裁剪 ，通过点击带提示的裁剪按钮\n![](images/2-1.png)\n\n可以进入图像裁剪的界面\n![](images/2-2.png)\n\n用户可以基于九宫格的选框，对图像进行裁剪。\n![](images/2-3.png)\n![](images/2-4.png)\n\n裁剪完之后，会在主界面显示截取之后的图像。\n![](images/2-5.png)\n\n当然，这只是最基本的裁剪功能，Monica 可以通过设置裁剪属性支持多种形式的裁剪。\n![](images/2-6.png)\n\n下面，我们以正六边形为裁剪框来裁剪图像\n![](images/2-7.png)\n![](images/2-8.png)\n\n接下来，还可以以爱心为裁剪框来裁剪图像\n![](images/2-9.png)\n![](images/2-10.png)\n\n\n## 2.3 图像绘制\n形状绘制的入口\n![](images/3-1.png)\n\n绘制形状的页面\n![](images/3-2.png)\n\nMonica 提供了图像上的任意位置绘制各种图形的功能，以及修改图形的属性比如图像的颜色、透明度、是否填充、边框类型。\n![](images/3-3.png)\n\n保存图像\n![](images/3-4.png)\n\n## 2.4 图像调色\nMonica 支持调节图像的对比度、色调、饱和度、亮度、色温等，从而帮助大家调整图像的色彩。\n\n图像调色的入口\n![](images/4-1.png)\n\n图像调色的界面\n![](images/4-2.png)\n\n支持拖动调节各个参数\n![](images/4-3.png)\n\n![](images/4-4.png)\n\n保存图像\n![](images/4-5.png)\n\nMonica 还支持通过 LLM 进行调色，主要是 deepseek、 gemini。用户每次输入一句自然语言指令，比如“肤色偏黄，冷一点”，就可以进行调色。也支持多轮对话和切换不同的大模型。\n\n![](images/4-6.png)\n\n![](images/4-7.png)\n\n![](images/4-8.png)\n\n![](images/4-9.png)\n\n![](images/4-10.png)\n\n## 2.5 滤镜\nMonica 支持多达 50 多款滤镜，大多数可以自行调整参数。\n![](images/5-1.png)\n\n如果需要修改滤镜的默认参数，可以直接修改。\n![](images/5-2.png)\n\n![](images/5-3.png)\n\n![](images/5-4.png)\n\n![](images/5-5.png)\n\n![](images/5-6.png)\n\n![](images/5-7.png)\n\n![](images/5-8.png)\n\n![](images/5-9.png)\n\n![](images/5-10.png)\n\n各种滤镜效果可以不断叠加，也可以跟其他功能一起使用。\n\n\n## 2.6 深度学习的算法\n在 AI 实验室有一些比较有意思的算法，比如人脸检测、生成素描画、人脸替换\n\n人脸检测包括:人脸、年龄、性别检测\n![](images/6-1.png)\n\n生成素描画的效果\n![](images/6-2.png)\n\n人脸替换\n![](images/6-3.png)\n\n\"人脸替换\"需要一张源图和加载一张目标图片。\n![](images/6-4.png)\n![](images/6-5.png)\n![](images/6-6.png)\n\n\"人脸替换\"也支持将目标图中所有的人脸进行替换。\n![](images/6-7.png)\n只需要设置一下替换 target 中人脸的数量即可。\n![](images/6-8.png)\n就可以完成目标图中所有的人脸替换。\n![](images/6-9.png)\n\n## 2.7 快速验证 OpenCV 算法的功能\n\n快速验证 OpenCV 算法的功能，更像是一个简单的快速调试开发工具，我做它的目的是为了快速验证一些算法，未来也不排除会把这个模块独立出来。\n\n下面是该模块的入口\n![](images/7-1.png)\n\n以及该模块的首页\n![](images/7-2.png)"
  },
  {
    "path": "LICENSE",
    "content": "Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "README-EN.md",
    "content": "**Monica** is a cross-platform desktop image editor.\nIt supports a wide range of image formats (including camera RAW), integrates both traditional image processing and deep learning–based image enhancement, and provides an extensible, developer-friendly editing experience.\n\n# 🧪 Tech Stack\n\n* **UI Framework**: Kotlin Compose Multiplatform (Desktop)\n* **Image Processing**: OpenCV\n* **Deep Learning Inference**: ONNX Runtime\n* **Backend Languages**: Kotlin / C++\n* **Build Tools**: Gradle / CMake\n\n# ✨ Features\n## 📷 Image Editing\n\n* Import: JPG, PNG, WebP, SVG, HDR, HEIC\n* Import camera RAW files: CR2, CR3, etc.\n* Export: JPG, PNG, WebP\n* Zoom and preview\n* Local blur & mosaic\n* Freehand drawing, shapes, and text annotations\n* Color picker\n* Geometric transforms: flip, rotate, scale, shear\n* Cropping with multiple shapes\n* Adjustments: contrast, hue, saturation, brightness, temperature, highlights, shadows\n* 50+ adjustable filters\n* Multi-image → GIF creation\n* Quick validation of OpenCV algorithms with parameter tuning\n\n## 🤖 AI-powered Enhancements\n\n* Face detection (face, gender, age)\n* Sketch generation from photos\n* Face replacement\n* Cartoonization with multiple styles\n\n# 📦 Installation & Usage\n## Run from Source\n\nUse IntelliJ IDEA / IntelliJ IDEA CE\n\n```bash\ngit clone https://github.com/fengzhizi715/Monica.git\ncd Monica\n./gradlew run\n```\n\n## Packaging\n\nRecommended packaging command:\n\n```bash\n./gradlew packageCurrentOsWithBundledWebRuntime\n```\n\nNotes:\n\n* Local development defaults to `isProVersion=false`\n* Packaging tasks automatically switch to `isProVersion=true`\n* macOS output: `build/output/main/dmg/`\n* Windows output: `build/output/main/exe/`\n* Linux output: `build/output/main/rpm/`\n\nIf you want to run platform-specific tasks directly:\n\n```bash\n./gradlew packageDmg\n./gradlew packageExe\n./gradlew packageRpm\n```\n\n## 🍎 macOS Packages\n\n### Intel Chip:\nMonica-x64-1.1.4.dmg\n\nDownload Link: https://pan.baidu.com/s/1ZS2e8krIh_kGUUEogMknrg?pwd=eyx7\n\n### Apple Silicon (M Series):\nMonica-arm64-1.1.4.dmg\n\nDownload Link: https://pan.baidu.com/s/1JJwT_UNFrQa-tUsAYywqkA?pwd=mngu\n\n## 🖥 Windows Package\n\nMonica-1.0.9.exe (latest version will be provided later, no Windows machine available now)\n\nDownload Link: https://pan.baidu.com/s/1jL0bL17Omxtc2rqOBn9yWg?pwd=5dii\n\n## 🐧 CentOS Package\n\nComing soon\n\n# 📸 Screenshots\n## ✨ New UI Preview\n\nSupport for **English UI + Multiple Themes**\n\nEnglish UI examples:\n\n![](images/screenshot-en1.png)\n\n![](images/screenshot-en2.png)\n\nTheme switching:\n![](images/ui-theme-settings.png)\n\nDark Theme:\n![](images/ui-theme-dark.png)\n\nPurple Theme:\n![](images/ui-theme-purple.png)\n\n## 📷 Classic Features\n\n![](images/screenshot.png)\n\n![](images/screenshot-version.png)\n\n![](images/4-2.png)\n\n![](images/5-2.png)\n\n![](images/7-2.png)\n\nMore screenshots 👉 [Feature Overview](FUNCTION.md)\n\nArticles 👉 [Juejin Column](https://juejin.cn/column/7396157773312065574)\n\n# 📁 CV & AI Services\n## ⚙️ CV Algorithms\n\nCode repo: https://github.com/fengzhizi715/MonicaImageProcess\n\nCurrently, prebuilt algorithm libraries are available for macOS and Windows. Kotlin calls them via JNI.\n\n|Library Name\t                        | Version | \tDescription\t  | Notes                           |\n|---------------------------------------|-------|---------------------|---------------------------------|\n|libMonicaImageProcess.dylib\t|0.2.3\t|Prebuilt for macOS\t| Built with CLion |\n|libopencv_world.4.10.0.dylib\t|–\t|OpenCV 4.10.0 prebuilt for macOS\t|Built with CMake |\n|MonicaImageProcess.dll\t        | 0.2.1\t|Prebuilt for Windows, depends on opencv_world481.dll|\tBuilt with Visual Studio 2022 |\n|opencv_world481.dll\t         | –\t|OpenCV 4.8.1 prebuilt for Windows\t| Built with Visual Studio 2022 |\n\n## ☁️ Deep Learning Services\n\nMonica communicates with deep learning inference services via HTTP.\nYou need to set the `Algorithm Service URL` in **General Settings**.\n\nSource code & models 👉 https://github.com/fengzhizi715/MonicaImageProcessHttpServer\n\n> No online deployment provided. Feel free to build and run locally.\n\n# 💻 Roadmap\n\n* - [x] Multi-format import/export\n* - [x] Core image editing features\n* - [x] AI module integration\n* - [ ] Plugin system support\n* - [ ] More AI features (face retouching, background removal, style transfer, etc.)\n\nUpcoming TODO:\n\n* Unified error handling\n* Improved configuration management\n* Enhanced cropping tools\n* Face retouching\n* Image compression\n* Upgrade Kotlin Compose Desktop & third-party libraries\n\n# 🤝 Contributing\n\nContributions of all kinds are welcome: new features, bug fixes, docs, or feedback.\n\n# 📄 License\n\nApache License 2.0\n\n# 📝 Changelog\n\nSee [CHANGELOG](CHANGELOG.md)\n\n# 📬 Contact\n\nWeChat: fengzhizi715\n\nEmail: fengzhizi715@126.com\n\n# 📈 Star History\n\n[![Star History Chart](https://api.star-history.com/svg?repos=fengzhizi715/Monica&type=Date)](https://star-history.com/#fengzhizi715/Monica&Date)\n"
  },
  {
    "path": "README.md",
    "content": "**Monica** 是一款跨平台的桌面图像编辑软件。它不仅支持多种图像格式（包括相机 RAW），还集成了传统图像处理和基于深度学习的图像增强功能，提供可扩展、可二次开发的图像编辑体验。\n\n# 🧪 技术栈\n* **UI 框架**：Kotlin Compose Multiplatform (Desktop)\n* **图像处理**：OpenCV\n* **深度学习推理**：ONNX Runtime\n* **后端语言**：Kotlin / C++\n* **构建工具**：Gradle / CMake\n\n# ✨ 功能列表\n## 📷 图像处理功能\n* 支持导入：JPG、PNG、WebP、SVG、HDR、HEIC\n* 支持相机 RAW 文件导入：CR2、CR3 等\n* 支持导出：JPG、PNG、WebP\n* 图像放大预览\n* 局部模糊、马赛克处理\n* 涂鸦、绘制形状、添加文字\n* 图像取色\n* 图像几何变换：翻转、旋转、缩放、错切\n* 支持各种形状的裁剪\n* 调整参数：对比度、色调、饱和度、亮度、色温、高光、阴影\n* 50+ 可调节滤镜\n* 多图合成 GIF\n* 快速验证 OpenCV 算法，支持简单算法的调参\n\n## 🤖 深度学习增强功能\n* 人脸检测(人脸、性别、年龄)\n* 图像生成素描画\n* 替换人脸\n* 多种风格的漫画生成\n\n# 📦 安装与运行\n\n## 从源码运行\n使用 IntelliJ IDEA / IntelliJ IDEA CE\n\n```bash\ngit clone https://github.com/fengzhizi715/Monica.git\ncd Monica\n./gradlew run\n```\n\n## 打包\n\n当前推荐直接使用统一打包命令：\n\n```bash\n./gradlew packageCurrentOsWithBundledWebRuntime\n```\n\n说明：\n\n* 本地开发默认使用 `isProVersion=false`\n* 打包任务会自动切换到 `isProVersion=true`\n* macOS 产物默认输出到 `build/output/main/dmg/`\n* Windows 产物默认输出到 `build/output/main/exe/`\n* Linux 产物默认输出到 `build/output/main/rpm/`\n\n如果需要直接调用平台任务，也可以使用：\n\n```bash\n./gradlew packageDmg\n./gradlew packageExe\n./gradlew packageRpm\n```\n\n## 🍎 macOS 安装包\n### Intel 芯片：\nMonica-x64-1.1.4.dmg\n\n链接: https://pan.baidu.com/s/1ZS2e8krIh_kGUUEogMknrg?pwd=eyx7\n\n### M 芯片：\nMonica-arm64-1.1.4.dmg\n\n链接: https://pan.baidu.com/s/1JJwT_UNFrQa-tUsAYywqkA?pwd=mngu\n\n## 🖥 Windows 安装包\nMonica-1.0.9.exe (最近没有 windows 电脑，稍后提供最新的版本)\n\n链接: https://pan.baidu.com/s/1jL0bL17Omxtc2rqOBn9yWg?pwd=5dii\n\n## 🐧 CentOS 安装包\n稍后提供\n\n# 📸 项目截图\n\n## ✨ UI 新版预览图\n支持 **英文版 UI + 多主题颜色切换**\n\n英文版界面示例\n![](images/screenshot-en1.png)\n\n![](images/screenshot-en2.png)\n\n主题切换\n![](images/ui-theme-settings.png)\n\n深色主题\n![](images/ui-theme-dark.png)\n\n紫色主题\n![](images/ui-theme-purple.png)\n\n## 📷 经典功能界面\n\n![](images/screenshot.png)\n\n![](images/screenshot-version.png)\n\n![](images/4-2.png)\n\n![](images/5-2.png)\n\n![](images/7-2.png)\n\n\n更多截图 👉 [详细功能介绍](FUNCTION.md)\n\n专栏文章 👉 [掘金专栏](https://juejin.cn/column/7396157773312065574)\n\n# 📁 CV 算法 && 深度学习的服务\n\n## ⚙️ CV 算法\n\nCV 算法的地址：\nhttps://github.com/fengzhizi715/MonicaImageProcess\n\n目前在 macOS、Windows 环境下编译好了相关的算法库，Kotlin 通过 jni 来调用该算法库。\n\n\n| 库名                                   | 版本号   | 描述                                                 | 备注                             |\n|---------------------------------------|-------|------------------------------------------------------|---------------------------------|\n| libMonicaImageProcess.dylib           | 0.2.3 | macOS 下编译好的算法库                                  | 使用 CLion 编译                  |\n| libopencv_world.4.10.0.dylib          |       | macOS 下基于 OpenCV 4.10.0 源码编译的 OpenCV 库          | 使用 cmake 编译                  |\n| MonicaImageProcess.dll                | 0.2.1 | Windows 下编译好的算法库需要依赖 opencv_world481.dll      | 使用 Visual Studio 2022 编译     |\n| opencv_world481.dll                   |       | Windows 下基于 OpenCV 4.8.1 源码编译的 OpenCV 库         | 使用 Visual Studio 2022 编译     |\n\n\n## ☁️ 深度学习的服务\n\nMonica 通过 HTTP 调用深度学习推理服务。需在 **通用设置** 中配置 `算法服务 URL`。\n\n源码与模型 👉 https://github.com/fengzhizi715/MonicaImageProcessHttpServer\n\n> 未部署线上服务，感兴趣可自行编译和部署\n\n\n# 💻 项目计划：\n* - [x] 多格式导入导出支持\n* - [x] 图像基础编辑功能\n* - [x] 深度学习模块集成\n* - [ ] 支持插件机制\n* - [ ] 添加更多 AI 功能（如人脸美颜、去背景、风格化等）\n\n近期的 TODO : \n\n* 优化滤镜模块，使用 LLM 实现自然语言使用滤镜\n* 完善配置管理\n* 优化图像裁剪的功能\n* 增加人脸美颜的功能\n* 增加插件机制\n* 升级 Kotlin Compose desktop、第三方库的版本\n\n\n# 🤝 贡献方式\n欢迎任何形式的贡献，包括但不限于功能开发、Bug 修复、文档完善和使用反馈。\n\n\n# 📄 开源协议\n本项目基于 Apache License 2.0 开源。\n\n\n# 📝 更新日志\n\n请查看 [CHANGELOG](CHANGELOG.md) 文件\n\n\n# 📬 联系方式：\n\nwechat：fengzhizi715\n\nEmail：fengzhizi715@126.com\n\n\n# 📈 Star History\n\n[![Star History Chart](https://api.star-history.com/svg?repos=fengzhizi715/Monica&type=Date)](https://star-history.com/#fengzhizi715/Monica&Date)\n"
  },
  {
    "path": "build.gradle.kts",
    "content": "import org.jetbrains.compose.desktop.application.dsl.TargetFormat\nimport java.io.File\nimport java.io.FileOutputStream\nimport java.net.URL\nimport java.security.MessageDigest\n\nplugins {\n    kotlin(\"multiplatform\")\n    id(\"org.jetbrains.compose\")\n    id(\"org.jetbrains.kotlin.plugin.compose\") version \"2.1.0\"\n}\n\n\ngroup = \"cn.netdiscovery.monica\"\nversion = \"${rootProject.extra[\"app.version\"]}\"\n\nval mOutputDir = project.buildDir.resolve(\"output\")\n\nrepositories {\n    google()\n    mavenCentral()\n    maven(\"https://maven.pkg.jetbrains.space/public/p/compose/dev\")\n    maven( \"https://jitpack.io\" )\n}\n\nval osName = System.getProperty(\"os.name\")\nval targetOs = when {\n    osName == \"Mac OS X\" -> \"macos\"\n    osName.startsWith(\"Win\") -> \"windows\"\n    osName.startsWith(\"Linux\") -> \"linux\"\n    else -> error(\"Unsupported OS: $osName\")\n}\n\nval osArch = System.getProperty(\"os.arch\")\nvar targetArch = when (osArch) {\n    \"x86_64\", \"amd64\" -> \"x64\"\n    \"aarch64\" -> \"arm64\"\n    else -> error(\"Unsupported arch: $osArch\")\n}\n\nval skikoVersion = \"0.8.4\"\nval target = \"${targetOs}-${targetArch}\"\nval resourcesRootDir = project.layout.projectDirectory.dir(\"resources\").asFile\nval bundledNodeVersion = providers.gradleProperty(\"bundledNodeVersion\").orElse(\"20.18.0\")\nval stagedWebRuntimeDir = layout.buildDirectory.dir(\"generated/web-screenshot-runtime/$target\")\nval packagedWebRuntimeDir = layout.buildDirectory.dir(\"generated/web-screenshot-payload/$target\")\nval packagedWebRuntimeZip = layout.buildDirectory.file(\"generated/web-screenshot-payload/$target/runtime.zip\")\nval packagedWebRuntimeResourceDir = File(resourcesRootDir, \"common/web-screenshot-runtime/$target\")\nval packagedWebRuntimeResourceZip = File(packagedWebRuntimeResourceDir, \"runtime.zip\")\n\nfun getBundledNodeDistPlatform(): String = when (targetOs) {\n    \"macos\" -> if (targetArch == \"arm64\") \"darwin-arm64\" else \"darwin-x64\"\n    \"windows\" -> if (targetArch == \"arm64\") \"win-arm64\" else \"win-x64\"\n    \"linux\" -> if (targetArch == \"arm64\") \"linux-arm64\" else \"linux-x64\"\n    else -> error(\"Unsupported target OS for bundled Node.js: $targetOs\")\n}\n\nfun getBundledNodeArchiveExtension(): String = if (targetOs == \"windows\") \"zip\" else \"tar.gz\"\n\nfun getBundledNodeExtractedDirName(version: String): String =\n    \"node-v$version-${getBundledNodeDistPlatform()}\"\n\nfun sha256(file: File): String {\n    val digest = MessageDigest.getInstance(\"SHA-256\")\n    file.inputStream().use { input ->\n        val buffer = ByteArray(DEFAULT_BUFFER_SIZE)\n        while (true) {\n            val read = input.read(buffer)\n            if (read <= 0) break\n            digest.update(buffer, 0, read)\n        }\n    }\n    return digest.digest().joinToString(\"\") { \"%02x\".format(it) }\n}\n\nfun findWebScreenshotRuntimeRoot(baseDir: File): File? {\n    val candidates = listOf(\n        baseDir,\n        File(baseDir, \"common\")\n    )\n\n    return candidates.firstOrNull { candidate ->\n        File(candidate, \"web-screenshot.js\").exists()\n    }\n}\n\nfun findBundledNodeExecutable(baseDir: File): File? {\n    val candidates = if (targetOs == \"windows\") {\n        listOf(\n            File(baseDir, \"$target/node/node.exe\"),\n            File(baseDir, \"$target/node.exe\")\n        )\n    } else {\n        listOf(\n            File(baseDir, \"$target/node/bin/node\"),\n            File(baseDir, \"$target/node/node\"),\n            File(baseDir, \"$target/bin/node\")\n        )\n    }\n\n    return candidates.firstOrNull { it.exists() }\n}\n\nfun findBundledNodeExecutableInRuntime(runtimeRoot: File): File? {\n    val candidates = if (targetOs == \"windows\") {\n        listOf(\n            File(runtimeRoot, \"node/node.exe\"),\n            File(runtimeRoot, \"node.exe\")\n        )\n    } else {\n        listOf(\n            File(runtimeRoot, \"node/bin/node\"),\n            File(runtimeRoot, \"node/node\"),\n            File(runtimeRoot, \"bin/node\")\n        )\n    }\n    return candidates.firstOrNull { it.exists() }\n}\n\nfun findPlaywrightBrowsersInRuntime(runtimeRoot: File): File? {\n    val candidates = listOf(\n        File(runtimeRoot, \"node_modules/playwright-core/.local-browsers\"),\n        File(runtimeRoot, \"ms-playwright\")\n    )\n    return candidates.firstOrNull { it.exists() }\n}\n\nfun hasInstalledPlaywrightBrowsersInRuntime(runtimeRoot: File): Boolean {\n    val browsersDir = findPlaywrightBrowsersInRuntime(runtimeRoot) ?: return false\n    val entries = browsersDir.listFiles().orEmpty().filter { !it.name.startsWith(\".\") }\n    val hasChromium = entries.any { it.name.startsWith(\"chromium-\") }\n    val hasHeadlessShell = entries.any { it.name.startsWith(\"chromium_headless_shell-\") }\n    val hasFfmpeg = entries.any { it.name.startsWith(\"ffmpeg-\") }\n    return hasChromium && hasHeadlessShell && hasFfmpeg\n}\n\nfun getNpmCliScript(nodeDir: File): File {\n    val script = File(nodeDir, \"lib/node_modules/npm/bin/npm-cli.js\")\n    if (!script.exists()) {\n        throw GradleException(\"Bundled npm CLI not found at ${script.absolutePath}\")\n    }\n    return script\n}\n\nfun getNpxCliScript(nodeDir: File): File {\n    val directScript = File(nodeDir, \"lib/node_modules/npm/bin/npx-cli.js\")\n    if (directScript.exists()) {\n        return directScript\n    }\n\n    val fallbackScript = File(nodeDir, \"lib/node_modules/npm/bin/npm-cli.js\")\n    if (fallbackScript.exists()) {\n        return fallbackScript\n    }\n\n    throw GradleException(\"Bundled npx/npm CLI not found under ${nodeDir.absolutePath}\")\n}\n\nfun getBundledNodeCommand(nodeDir: File): File {\n    val executable = if (targetOs == \"windows\") {\n        File(nodeDir, \"node.exe\")\n    } else {\n        File(nodeDir, \"bin/node\")\n    }\n\n    if (!executable.exists()) {\n        throw GradleException(\"Bundled Node.js executable not found at ${executable.absolutePath}\")\n    }\n\n    return executable\n}\n\nkotlin {\n    jvm {\n        withJava()\n    }\n    sourceSets {\n        val jvmMain by getting {\n            dependencies {\n                implementation(compose.desktop.currentOs)\n                implementation(project(\":domain\"))\n                implementation(project(\":config\"))\n                implementation(project(\":imageprocess\"))\n                implementation(project(\":opencv\"))\n                implementation(project(\":i18n\"))\n\n                implementation (\"org.jetbrains.kotlin:kotlin-reflect\")\n\n                // skiko\n                implementation(\"org.jetbrains.skiko:skiko-awt-runtime-$target:$skikoVersion\")\n\n                // 缓存\n                implementation(\"com.github.fengzhizi715.RxCache:core:${rootProject.extra[\"rxcache\"]}\")\n                implementation(\"com.github.fengzhizi715.RxCache:okio:${rootProject.extra[\"rxcache\"]}\")\n                implementation(\"com.github.fengzhizi715.RxCache:extension:${rootProject.extra[\"rxcache\"]}\")\n\n                // di\n                implementation(\"io.insert-koin:koin-compose:${rootProject.extra[\"koin.compose\"]}\")\n\n                // color math\n                implementation(\"com.github.ajalt.colormath:colormath-ext-jetpack-compose:${rootProject.extra[\"colormath\"]}\")\n\n                // coroutines utils\n                implementation (\"com.github.fengzhizi715.Kotlin-Coroutines-Utils:common:${rootProject.extra[\"coroutines.utils\"]}\")\n                // 为 Desktop/Swing 提供 Dispatchers.Main（绑定到 EDT）\n                implementation(\"org.jetbrains.kotlinx:kotlinx-coroutines-swing:${rootProject.extra[\"kotlinx.coroutines.core.version\"]}\")\n\n                // log config\n                implementation(\"ch.qos.logback:logback-classic:${rootProject.extra[\"logback\"]}\")\n                implementation(\"ch.qos.logback:logback-core:${rootProject.extra[\"logback\"]}\")\n                implementation(\"ch.qos.logback:logback-access:${rootProject.extra[\"logback\"]}\")\n\n                // okhttp-extension\n                implementation(\"com.github.fengzhizi715.okhttp-extension:core:1.3.2\")\n                implementation(\"com.github.fengzhizi715.okhttp-logging-interceptor:core:v1.1.4\")\n                implementation (\"com.squareup.okhttp3:okhttp:4.10.0\")\n                implementation (\"com.google.code.gson:gson:2.10.1\")\n                implementation (\"org.json:json:20240303\")\n\n                // generate gif\n                implementation (\"com.madgag:animated-gif-lib:1.4\")\n\n                // sqlite\n                implementation(\"org.xerial:sqlite-jdbc:3.50.3.0\")\n            }\n        }\n        val jvmTest by getting {\n            dependencies {\n                implementation(kotlin(\"test\"))\n            }\n        }\n    }\n}\n\nval verifyBundledWebScreenshotRuntime by tasks.registering {\n    group = \"verification\"\n    description = \"Verify offline web screenshot payload resources for desktop packaging.\"\n\n    doLast {\n        val runtimeRoot = findWebScreenshotRuntimeRoot(resourcesRootDir)\n            ?: throw GradleException(\n                \"Missing web screenshot runtime root. Expected web-screenshot.js under \" +\n                    \"${resourcesRootDir.absolutePath} or ${File(resourcesRootDir, \"common\").absolutePath}.\"\n            )\n\n        val missing = mutableListOf<String>()\n\n        if (!File(runtimeRoot, \"package.json\").exists()) {\n            missing += \"${runtimeRoot.absolutePath}/package.json\"\n        }\n        val payloadZip = packagedWebRuntimeResourceZip\n        if (!payloadZip.exists()) {\n            missing += payloadZip.absolutePath\n        }\n\n        if (missing.isNotEmpty()) {\n            throw GradleException(\n                buildString {\n                    appendLine(\"Bundled web screenshot runtime is incomplete for target '$target'.\")\n                    appendLine(\"Missing resources:\")\n                    missing.forEach { appendLine(\"- $it\") }\n                    appendLine(\"Suggested setup:\")\n                    appendLine(\"1. Prepare the offline runtime payload\")\n                    appendLine(\"   ./gradlew prepareBundledWebScreenshotRuntime\")\n                    appendLine(\"2. Re-run packaging\")\n                }\n            )\n        }\n    }\n}\n\ntasks.matching {\n    it.name in setOf(\n        \"createDistributable\",\n        \"packageDistributionForCurrentOS\",\n        \"packageDmg\",\n        \"packageMsi\",\n        \"packageExe\",\n        \"packageRpm\"\n    )\n}.configureEach {\n    dependsOn(verifyBundledWebScreenshotRuntime)\n}\n\nval downloadBundledNode by tasks.registering {\n    group = \"distribution\"\n    description = \"Download and unpack bundled Node.js into the staged offline runtime payload.\"\n\n    val version = bundledNodeVersion.get()\n    val distPlatform = getBundledNodeDistPlatform()\n    val archiveExtension = getBundledNodeArchiveExtension()\n    val archiveFile = layout.buildDirectory.file(\"tmp/bundled-node/node-v$version-$distPlatform.$archiveExtension\")\n    val extractDir = layout.buildDirectory.dir(\"tmp/bundled-node/extracted/$target\")\n    val targetNodeDir = stagedWebRuntimeDir.map { it.dir(\"node\").asFile }\n\n    doLast {\n        val versionValue = bundledNodeVersion.get()\n        val runtimeRoot = stagedWebRuntimeDir.get().asFile\n        val nodeRoot = targetNodeDir.get()\n        val versionMarker = File(nodeRoot, \".bundled-node-version\")\n        val existingNode = findBundledNodeExecutableInRuntime(runtimeRoot)\n        if (existingNode != null && versionMarker.exists() && versionMarker.readText().trim() == versionValue) {\n            logger.lifecycle(\"Bundled Node.js already prepared at ${nodeRoot.absolutePath}, skipping download.\")\n            return@doLast\n        }\n\n        val downloadUrl = \"https://nodejs.org/dist/v$versionValue/${getBundledNodeExtractedDirName(versionValue)}.$archiveExtension\"\n        val archive = archiveFile.get().asFile\n        val extractedRoot = extractDir.get().asFile\n\n        archive.parentFile.mkdirs()\n        extractedRoot.mkdirs()\n\n        logger.lifecycle(\"Downloading bundled Node.js from $downloadUrl\")\n        URL(downloadUrl).openStream().use { input ->\n            FileOutputStream(archive).use { output ->\n                input.copyTo(output)\n            }\n        }\n\n        project.delete(extractedRoot)\n        extractedRoot.mkdirs()\n\n        copy {\n            from(\n                if (archiveExtension == \"zip\") {\n                    zipTree(archive)\n                } else {\n                    tarTree(resources.gzip(archive))\n                }\n            )\n            into(extractedRoot)\n        }\n\n        val extractedNodeDir = File(extractedRoot, getBundledNodeExtractedDirName(versionValue))\n        if (!extractedNodeDir.exists()) {\n            throw GradleException(\"Downloaded Node.js archive did not contain ${extractedNodeDir.absolutePath}\")\n        }\n\n        project.delete(nodeRoot)\n        nodeRoot.parentFile.mkdirs()\n\n        copy {\n            from(extractedNodeDir)\n            into(nodeRoot)\n        }\n\n        if (targetOs != \"windows\") {\n            listOf(\n                File(nodeRoot, \"bin/node\"),\n                File(nodeRoot, \"node\")\n            ).filter { it.exists() }.forEach { file ->\n                file.setExecutable(true)\n            }\n        }\n\n        versionMarker.writeText(\"$versionValue\\n\")\n\n        logger.lifecycle(\"Bundled Node.js prepared at ${nodeRoot.absolutePath}\")\n    }\n}\n\nval installBundledPlaywrightRuntime by tasks.registering {\n    group = \"distribution\"\n    description = \"Stage the offline Playwright runtime that will be extracted on first use.\"\n\n    dependsOn(downloadBundledNode)\n    val sourceRuntimeRoot = findWebScreenshotRuntimeRoot(resourcesRootDir) ?: resourcesRootDir\n    val runtimeRoot = stagedWebRuntimeDir.get().asFile\n    val lockFile = File(sourceRuntimeRoot, \"package-lock.json\")\n    val packageFile = File(sourceRuntimeRoot, \"package.json\")\n    val runtimeStampFile = File(runtimeRoot, \".playwright-runtime-stamp\")\n\n    inputs.file(packageFile)\n\n    doLast {\n        runtimeRoot.mkdirs()\n\n        copy {\n            from(File(sourceRuntimeRoot, \"web-screenshot.js\"))\n            from(packageFile)\n            if (lockFile.exists()) {\n                from(lockFile)\n            }\n            into(runtimeRoot)\n        }\n\n        val nodeDir = findBundledNodeExecutableInRuntime(runtimeRoot)?.parentFile?.let { parent ->\n            if (targetOs == \"windows\") parent else parent.parentFile\n        } ?: throw GradleException(\"Bundled Node.js is missing. Run downloadBundledNode first.\")\n\n        val lockHash = if (lockFile.exists()) sha256(lockFile) else sha256(packageFile)\n        val expectedStamp = buildString {\n            append(\"node=\")\n            append(bundledNodeVersion.get())\n            append('\\n')\n            append(\"lock=\")\n            append(lockHash)\n            append('\\n')\n            append(\"target=\")\n            append(target)\n            append('\\n')\n        }\n\n        if (runtimeStampFile.exists() &&\n            runtimeStampFile.readText() == expectedStamp &&\n            File(runtimeRoot, \"node_modules/playwright/package.json\").exists() &&\n            File(runtimeRoot, \"node_modules/playwright-core/package.json\").exists() &&\n            hasInstalledPlaywrightBrowsersInRuntime(runtimeRoot)\n        ) {\n            logger.lifecycle(\"Bundled Playwright runtime already installed in ${runtimeRoot.absolutePath}, skipping npm install.\")\n            return@doLast\n        }\n\n        val nodeExecutable = getBundledNodeCommand(nodeDir)\n        val npmCli = getNpmCliScript(nodeDir)\n        val npxCli = getNpxCliScript(nodeDir)\n\n        logger.lifecycle(\"Installing web screenshot npm dependencies in ${runtimeRoot.absolutePath}\")\n        exec {\n            workingDir = runtimeRoot\n            executable = nodeExecutable.absolutePath\n            args = listOf(npmCli.absolutePath, \"ci\")\n            environment(\"PLAYWRIGHT_BROWSERS_PATH\", \"0\")\n        }\n\n        logger.lifecycle(\"Installing bundled Chromium in ${runtimeRoot.absolutePath}\")\n        val playwrightInstallArgs = if (npxCli.name == \"npm-cli.js\") {\n            listOf(npxCli.absolutePath, \"exec\", \"playwright\", \"install\", \"chromium\")\n        } else {\n            listOf(npxCli.absolutePath, \"playwright\", \"install\", \"chromium\")\n        }\n        exec {\n            workingDir = runtimeRoot\n            executable = nodeExecutable.absolutePath\n            args = playwrightInstallArgs\n            environment(\"PLAYWRIGHT_BROWSERS_PATH\", \"0\")\n        }\n\n        runtimeStampFile.writeText(expectedStamp)\n    }\n}\n\nval packageBundledWebScreenshotRuntime by tasks.registering(Zip::class) {\n    group = \"distribution\"\n    description = \"Create the offline runtime zip that the app will extract on first screenshot use.\"\n\n    dependsOn(installBundledPlaywrightRuntime)\n    from(stagedWebRuntimeDir)\n    destinationDirectory.set(packagedWebRuntimeDir)\n    archiveFileName.set(\"runtime.zip\")\n}\n\nval cleanupLegacyWebScreenshotPackagingResources by tasks.registering {\n    group = \"distribution\"\n    description = \"Remove legacy bundled web screenshot executables from source resources so packaging stays close to main.\"\n\n    doNotTrackState(\"This task cleans legacy packaging artifacts from source resources in place.\")\n\n    doLast {\n        listOf(\n            File(resourcesRootDir, \"node_modules\"),\n            File(resourcesRootDir, \"common/node_modules\"),\n            File(resourcesRootDir, \"ms-playwright\"),\n            File(resourcesRootDir, \"common/ms-playwright\"),\n            File(resourcesRootDir, \".playwright-runtime-stamp\"),\n            File(resourcesRootDir, \"macos-arm64/node\"),\n            File(resourcesRootDir, \"macos-x64/node\"),\n            File(resourcesRootDir, \"linux-arm64/node\"),\n            File(resourcesRootDir, \"linux-x64/node\"),\n            File(resourcesRootDir, \"windows/node\")\n        ).forEach { path ->\n            if (path.exists()) {\n                project.delete(path)\n            }\n        }\n    }\n}\n\nval preparePackagedResources by tasks.registering {\n    group = \"distribution\"\n    description = \"Write the offline web screenshot payload into standard resources layout.\"\n\n    dependsOn(packageBundledWebScreenshotRuntime, cleanupLegacyWebScreenshotPackagingResources)\n    doNotTrackState(\"This task prepares standard source resources in place for Compose Desktop packaging.\")\n\n    doLast {\n        packagedWebRuntimeResourceDir.mkdirs()\n        copy {\n            from(packagedWebRuntimeZip)\n            into(packagedWebRuntimeResourceDir)\n        }\n    }\n}\n\nval prepareBundledWebScreenshotRuntime by tasks.registering {\n    group = \"distribution\"\n    description = \"Prepare the offline web screenshot payload for packaging.\"\n\n    dependsOn(preparePackagedResources)\n}\n\nval cleanNativeDistributionOutputs by tasks.registering {\n    group = \"distribution\"\n    description = \"Remove stale native distribution outputs so old app resources are not reused across packaging runs.\"\n\n    doNotTrackState(\"This task deletes generated packaging outputs before creating new native bundles.\")\n\n    doLast {\n        listOf(\n            File(mOutputDir, \"main/app\"),\n            File(mOutputDir, \"main/dmg\"),\n            File(mOutputDir, \"main/msi\"),\n            File(mOutputDir, \"main/exe\"),\n            File(mOutputDir, \"main/rpm\")\n        ).forEach { dir ->\n            if (dir.exists()) {\n                project.delete(dir)\n            }\n        }\n    }\n}\n\nverifyBundledWebScreenshotRuntime.configure {\n    dependsOn(prepareBundledWebScreenshotRuntime)\n}\n\nval packageCurrentOsWithBundledWebRuntime by tasks.registering {\n    group = \"distribution\"\n    description = \"Prepare bundled web screenshot runtime and package for the current OS.\"\n\n    val packageTaskName = when (targetOs) {\n        \"macos\" -> \"packageDmg\"\n        \"windows\" -> \"packageExe\"\n        \"linux\" -> \"packageRpm\"\n        else -> error(\"Unsupported target OS: $targetOs\")\n    }\n\n    dependsOn(packageTaskName)\n}\n\nval currentOsPackageTaskName = when (targetOs) {\n    \"macos\" -> \"packageDmg\"\n    \"windows\" -> \"packageExe\"\n    \"linux\" -> \"packageRpm\"\n    else -> error(\"Unsupported target OS: $targetOs\")\n}\n\ntasks.matching { it.name == currentOsPackageTaskName }.configureEach {\n    dependsOn(cleanNativeDistributionOutputs, prepareBundledWebScreenshotRuntime)\n}\n\njava {\n    toolchain {\n        languageVersion.set(JavaLanguageVersion.of(17))\n    }\n}\n\ncompose.desktop {\n    application {\n        mainClass = \"MainKt\"\n        buildTypes.release.proguard {\n            configurationFiles.from(project.file(\"compose-desktop.pro\"))\n        }\n        nativeDistributions {\n            outputBaseDir.set(mOutputDir)   //build/output\n            targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Exe, TargetFormat.Rpm)\n            appResourcesRootDir.set(project.layout.projectDirectory.dir(\"resources\"))\n            packageName = \"Monica-$targetArch\"\n            packageVersion = \"${rootProject.extra[\"app.version\"]}\"\n            description = \"Monica is a cross-platform image editor\"\n            copyright = \"© 2024 Tony Shen. All rights reserved.\"\n\n            jvmArgs += listOf(\"-Xms4G\",\"-Xmx4G\")\n            jvmArgs += listOf(\"-Dlogback.debug=true\")\n\n            includeAllModules = true    //包含所有模块\n\n            macOS {\n                bundleID = \"cn.netdiscovery.monica\"\n                dockName = \"monica\"\n            }\n\n            windows {\n                console = false    // 为应用程序添加一个控制台启动器\n                shortcut = true    // 桌面快捷方式\n                dirChooser = true  // 允许在安装过程中自定义安装路径\n                perUserInstall = false   //允许在每个用户的基础上安装应用程序\n                menuGroup = \"start-menu-group\"\n                upgradeUuid = \"b329caf3-6681-49b9-98d0-adb34d32e130\"\n                iconFile.set(project.file(\"src/jvmMain/resources/images/launcher.ico\"))\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "compose-desktop.pro",
    "content": "-dontwarn org.slf4j.**\n-dontwarn ch.qos.logback.**\n-dontwarn com.google.gson.**\n-dontwarn org.apache.**\n-dontwarn com.safframework.rxcache.**\n\n-keep class cn.netdiscovery.monica.** {*;}\n-keep interface ccn.netdiscovery.monica.** {*;}\n-keep enum cn.netdiscovery.monica.** {*;}"
  },
  {
    "path": "config/build.gradle.kts",
    "content": " plugins {\n    kotlin(\"jvm\")\n    id(\"com.github.gmazzo.buildconfig\") version \"5.4.0\"\n}\n\nrepositories {\n    mavenCentral()\n    maven { url = uri(\"https://jitpack.io\") }\n}\n\nval requestedTaskNames = gradle.startParameter.taskNames\n\nval isPackagingBuild = requestedTaskNames.any { taskName ->\n    val normalized = taskName.substringAfterLast(':')\n    normalized in setOf(\n        \"packageCurrentOsWithBundledWebRuntime\",\n        \"packageDmg\",\n        \"packageMsi\",\n        \"packageExe\",\n        \"packageRpm\",\n        \"packageDistributionForCurrentOS\",\n        \"createDistributable\"\n    )\n}\n\nval isProVersion = providers.gradleProperty(\"isProVersion\")\n    .map(String::toBoolean)\n    .orElse(isPackagingBuild)\n    .get()\n\nbuildConfig {\n    useKotlinOutput { topLevelConstants = true }\n    useKotlinOutput { internalVisibility = false }   // adds `internal` modifier to all declarations\n\n    buildConfigField(\"APP_NAME\", project.name)\n    buildConfigField(\"APP_VERSION\", \"${rootProject.extra[\"app.version\"]}\")\n    buildConfigField(\"KOTLIN_VERSION\", \"${rootProject.extra[\"kotlin.version\"]}\")\n    buildConfigField(\"COMPOSE_VERSION\", \"${rootProject.extra[\"compose.version\"]}\")\n    buildConfigField(\"IS_PRO_VERSION\", isProVersion)\n    buildConfigField(\"BUILD_TIME\", System.currentTimeMillis())\n}\n\ndependencies {\n    testImplementation(kotlin(\"test\"))\n    implementation (\"org.jetbrains.kotlin:kotlin-stdlib\")\n    \n    // Logging\n    implementation(\"ch.qos.logback:logback-classic:${rootProject.extra[\"logback\"]}\")\n    implementation(\"ch.qos.logback:logback-core:${rootProject.extra[\"logback\"]}\")\n    \n    // Gson for JSON serialization\n    implementation(\"com.google.code.gson:gson:2.10.1\")\n    \n    // Domain module (for GeneralSettings)\n    implementation(project(\":domain\"))\n    \n    // RxCache (for type definitions, instance will be provided by main project)\n    implementation(\"com.github.fengzhizi715.RxCache:core:${rootProject.extra[\"rxcache\"]}\")\n    implementation(\"com.github.fengzhizi715.RxCache:extension:${rootProject.extra[\"rxcache\"]}\")\n}\n\ntasks.test {\n    useJUnitPlatform()\n}\nkotlin {\n    jvmToolchain(17)\n}\n"
  },
  {
    "path": "config/src/main/kotlin/cn/netdiscovery/monica/config/Constants.kt",
    "content": "package cn.netdiscovery.monica.config\n\nimport java.text.SimpleDateFormat\n\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.config.Constants\n * @author: Tony Shen\n * @date: 2024/5/7 10:55\n * @version: V1.0 <描述当前版本功能>\n */\nval appVersion by lazy {\n    Monica.config.BuildConfig.APP_VERSION\n}\n\nval kotlinVersion by lazy {\n    Monica.config.BuildConfig.KOTLIN_VERSION\n}\n\nval composeVersion by lazy {\n    Monica.config.BuildConfig.COMPOSE_VERSION\n}\n\nval isProVersion by lazy {\n    Monica.config.BuildConfig.IS_PRO_VERSION\n}\n\nval buildTime:String by lazy {\n   val time = Monica.config.BuildConfig.BUILD_TIME\n\n    val dateformat = SimpleDateFormat(\"yyyy-MM-dd HH:mm:ss\")\n    dateformat.format(time)\n}\n\n\nconst val KEY_CROP_FIRST = \"key_crop_first\"\nconst val KEY_CROP_SECOND = \"key_crop_second\"\nconst val KEY_CROP = \"key_crop\"\nconst val KEY_GENERAL_SETTINGS = \"key_general_settings\"\n\nconst val STATUS_HTTP_SERVER_OK = 1\nconst val STATUS_HTTP_SERVER_FAILED = 0\n\nconst val MODULE_COLOR = \"module_color\"\nconst val MODULE_OPENCV = \"module_opencv\""
  },
  {
    "path": "config/src/main/kotlin/cn/netdiscovery/monica/config/SystemConstants.kt",
    "content": "package cn.netdiscovery.monica.config\n\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.utils.SystemUtils\n * @author: Tony Shen\n * @date:  2024/7/6 14:36\n * @version: V1.0 <描述当前版本功能>\n */\n\nval os: String = System.getProperty(\"os.name\")\nval arch: String = System.getProperty(\"os.arch\")\nval osVersion: String = System.getProperty(\"os.version\")\nval javaVersion: String = System.getProperty(\"java.version\")\nval javaVendor: String = System.getProperty(\"java.vendor\")\nval workDirectory: String = System.getProperty(\"user.dir\")\nval userHome: String = System.getProperty(\"user.home\")\n\nval isMac by lazy {\n    os.contains(\"Mac\")\n}\n\nval isWindows by lazy {\n    os.startsWith(\"Win\")\n}\n\nval isLinux by lazy {\n    os.contains(\"nux\") || os.contains(\"nix\")\n}"
  },
  {
    "path": "config/src/main/kotlin/cn/netdiscovery/monica/config/category/ConfigCategory.kt",
    "content": "package cn.netdiscovery.monica.config.category\n\n/**\n * 配置分类枚举\n * \n * 用于区分不同类型的配置，便于统一管理和验证。\n * \n * @author: Tony Shen\n * @date: 2025-12-12\n */\nenum class ConfigCategory {\n    /**\n     * 应用设置（用户可修改的设置，如 GeneralSettings）\n     */\n    APP_SETTINGS,\n    \n    /**\n     * UI 配置（UI 相关的配置，如滤镜参数元数据）\n     */\n    UI_CONFIG,\n    \n    /**\n     * 业务配置（业务逻辑相关的配置，如 API 密钥、算法 URL）\n     */\n    BUSINESS_CONFIG,\n    \n    /**\n     * 用户偏好（用户个人偏好设置，如语言、主题）\n     */\n    USER_PREFERENCE,\n    \n    /**\n     * 临时配置（临时存储的配置，如裁剪状态）\n     */\n    TEMPORARY\n}\n\n"
  },
  {
    "path": "config/src/main/kotlin/cn/netdiscovery/monica/config/category/ConfigCategoryManager.kt",
    "content": "package cn.netdiscovery.monica.config.category\n\nimport cn.netdiscovery.monica.config.storage.ConfigManager\nimport cn.netdiscovery.monica.config.storage.ConfigStorage\nimport cn.netdiscovery.monica.config.storage.ConfigType\nimport org.slf4j.Logger\nimport org.slf4j.LoggerFactory\n\n/**\n * 配置分类管理器\n * \n * 根据配置分类选择合适的存储和验证策略。\n * \n * @author: Tony Shen\n * @date: 2025-12-12\n */\nobject ConfigCategoryManager {\n    \n    private val logger: Logger = LoggerFactory.getLogger(ConfigCategoryManager::class.java)\n    \n    /**\n     * 配置元信息\n     */\n    data class ConfigMetadata<T>(\n        val key: String,\n        val category: ConfigCategory,\n        val storageType: ConfigType,\n        val validator: ConfigValidator<T>? = null,\n        val defaultValue: T\n    )\n    \n    /**\n     * 配置注册表\n     */\n    private val configRegistry = mutableMapOf<String, ConfigMetadata<*>>()\n    \n    /**\n     * 注册配置\n     */\n    fun <T> register(metadata: ConfigMetadata<T>) {\n        configRegistry[metadata.key] = metadata\n        logger.debug(\"Registered config: key=${metadata.key}, category=${metadata.category}\")\n    }\n    \n    /**\n     * 获取配置元信息\n     */\n    @Suppress(\"UNCHECKED_CAST\")\n    fun <T> getMetadata(key: String): ConfigMetadata<T>? {\n        return configRegistry[key] as? ConfigMetadata<T>\n    }\n    \n    /**\n     * 保存配置（带验证）\n     */\n    fun <T> save(key: String, value: T): ValidationResult {\n        val metadata = getMetadata<T>(key)\n        \n        // 验证配置值\n        metadata?.validator?.let { validator ->\n            val error = validator.validate(value)\n            if (error != null) {\n                logger.warn(\"Config validation failed: key=$key, error=$error\")\n                return ValidationResult.failure(error)\n            }\n        }\n        \n        // 保存配置\n        try {\n            val storageType = metadata?.storageType ?: ConfigType.DEFAULT\n            ConfigManager.save(key, value, storageType)\n            logger.debug(\"Config saved: key=$key, category=${metadata?.category}\")\n            return ValidationResult.success()\n        } catch (e: Exception) {\n            logger.error(\"Failed to save config: key=$key\", e)\n            return ValidationResult.failure(\"Failed to save config: ${e.message}\")\n        }\n    }\n    \n    /**\n     * 加载配置（带默认值）\n     */\n    @Suppress(\"UNCHECKED_CAST\")\n    fun <T> load(key: String): T? {\n        val metadata = getMetadata<T>(key)\n        val storageType = metadata?.storageType ?: ConfigType.DEFAULT\n        val defaultValue = metadata?.defaultValue\n        \n        return if (defaultValue != null) {\n            ConfigManager.load(key, defaultValue, storageType) as T\n        } else {\n            logger.warn(\"No default value for config: key=$key\")\n            null\n        }\n    }\n    \n    /**\n     * 加载配置（带自定义默认值）\n     */\n    fun <T> load(key: String, default: T): T {\n        val metadata = getMetadata<T>(key)\n        val storageType = metadata?.storageType ?: ConfigType.DEFAULT\n        return ConfigManager.load(key, default, storageType)\n    }\n    \n    /**\n     * 验证配置值（不保存）\n     */\n    fun <T> validate(key: String, value: T): ValidationResult {\n        val metadata = getMetadata<T>(key)\n        val validator = metadata?.validator ?: return ValidationResult.success()\n        \n        val error = validator.validate(value)\n        return if (error != null) {\n            ValidationResult.failure(error)\n        } else {\n            ValidationResult.success()\n        }\n    }\n    \n    /**\n     * 获取配置的存储类型\n     */\n    fun getStorageType(key: String): ConfigType {\n        return getMetadata<Any>(key)?.storageType ?: ConfigType.DEFAULT\n    }\n    \n    /**\n     * 获取配置的分类\n     */\n    fun getCategory(key: String): ConfigCategory? {\n        return getMetadata<Any>(key)?.category\n    }\n    \n    /**\n     * 获取所有已注册的配置键\n     */\n    fun getAllKeys(): List<String> {\n        return configRegistry.keys.toList()\n    }\n    \n    /**\n     * 获取指定分类的所有配置键\n     */\n    fun getKeysByCategory(category: ConfigCategory): List<String> {\n        return configRegistry.filter { it.value.category == category }.keys.toList()\n    }\n    \n    /**\n     * 清除指定分类的所有配置\n     */\n    fun clearCategory(category: ConfigCategory) {\n        val keys = getKeysByCategory(category)\n        keys.forEach { key ->\n            val metadata = getMetadata<Any>(key)\n            val storageType = metadata?.storageType ?: ConfigType.DEFAULT\n            ConfigManager.remove(key, storageType)\n        }\n        logger.info(\"Cleared all configs in category: $category\")\n    }\n}\n\n"
  },
  {
    "path": "config/src/main/kotlin/cn/netdiscovery/monica/config/category/ConfigDefinitions.kt",
    "content": "package cn.netdiscovery.monica.config.category\n\nimport cn.netdiscovery.monica.config.KEY_GENERAL_SETTINGS\nimport cn.netdiscovery.monica.config.storage.ConfigType\nimport cn.netdiscovery.monica.domain.GeneralSettings\n\n/**\n * 配置定义\n * \n * 集中定义所有配置的元信息，包括分类、存储类型、验证规则和默认值。\n * \n * @author: Tony Shen\n * @date: 2025-12-12\n */\nobject ConfigDefinitions {\n    \n    /**\n     * 初始化所有配置定义\n     */\n    fun initialize() {\n        registerAppSettings()\n        registerUserPreferences()\n        registerTemporaryConfigs()\n    }\n    \n    /**\n     * 注册应用设置\n     */\n    private fun registerAppSettings() {\n        // GeneralSettings\n        ConfigCategoryManager.register(\n            ConfigCategoryManager.ConfigMetadata(\n                key = KEY_GENERAL_SETTINGS,\n                category = ConfigCategory.APP_SETTINGS,\n                storageType = ConfigType.RX_CACHE,\n                defaultValue = GeneralSettings(\n                    outputBoxR = 255,\n                    outputBoxG = 255,\n                    outputBoxB = 255,\n                    size = 512,\n                    maxHistorySize = 50,\n                    deepSeekApiKey = \"\",\n                    geminiApiKey = \"\",\n                    algorithmUrl = \"\",\n                    themeId = \"LIGHT\"\n                )\n            )\n        )\n    }\n    \n    /**\n     * 注册用户偏好设置\n     */\n    private fun registerUserPreferences() {\n        // 语言设置\n        ConfigCategoryManager.register(\n            ConfigCategoryManager.ConfigMetadata(\n                key = \"selected_language\",\n                category = ConfigCategory.USER_PREFERENCE,\n                storageType = ConfigType.PREFERENCES,\n                defaultValue = \"zh\"\n            )\n        )\n    }\n    \n    /**\n     * 注册临时配置\n     */\n    private fun registerTemporaryConfigs() {\n        // 裁剪相关临时配置已在 Constants.kt 中定义\n        // 这里可以添加其他临时配置的定义\n    }\n}"
  },
  {
    "path": "config/src/main/kotlin/cn/netdiscovery/monica/config/category/ConfigValidator.kt",
    "content": "package cn.netdiscovery.monica.config.category\n\nimport org.slf4j.Logger\nimport org.slf4j.LoggerFactory\n\n/**\n * 配置验证器接口\n * \n * 用于验证配置值的有效性。\n * \n * @author: Tony Shen\n * @date: 2025-12-12\n */\ninterface ConfigValidator<T> {\n    /**\n     * 验证配置值\n     * \n     * @param value 配置值\n     * @return 验证结果，如果有效返回 null，否则返回错误信息\n     */\n    fun validate(value: T): String?\n}\n\n/**\n * 配置验证结果\n */\ndata class ValidationResult(\n    val isValid: Boolean,\n    val errorMessage: String? = null\n) {\n    companion object {\n        fun success() = ValidationResult(true)\n        fun failure(message: String) = ValidationResult(false, message)\n    }\n}\n\n/**\n * 通用配置验证器\n */\nobject CommonValidators {\n    \n    private val logger: Logger = LoggerFactory.getLogger(CommonValidators::class.java)\n    \n    /**\n     * 字符串非空验证器\n     */\n    fun nonEmptyString(): ConfigValidator<String> = object : ConfigValidator<String> {\n        override fun validate(value: String): String? {\n            return if (value.isBlank()) {\n                \"Value cannot be empty\"\n            } else {\n                null\n            }\n        }\n    }\n    \n    /**\n     * 字符串长度验证器\n     */\n    fun stringLength(min: Int, max: Int): ConfigValidator<String> = object : ConfigValidator<String> {\n        override fun validate(value: String): String? {\n            return when {\n                value.length < min -> \"Value length must be at least $min\"\n                value.length > max -> \"Value length must be at most $max\"\n                else -> null\n            }\n        }\n    }\n    \n    /**\n     * 数值范围验证器\n     */\n    fun <T : Number> numberRange(min: T, max: T): ConfigValidator<T> = object : ConfigValidator<T> {\n        override fun validate(value: T): String? {\n            val doubleValue = value.toDouble()\n            val minValue = min.toDouble()\n            val maxValue = max.toDouble()\n            return when {\n                doubleValue < minValue -> \"Value must be at least $min\"\n                doubleValue > maxValue -> \"Value must be at most $max\"\n                else -> null\n            }\n        }\n    }\n    \n    /**\n     * 整数范围验证器\n     */\n    fun intRange(min: Int, max: Int): ConfigValidator<Int> = object : ConfigValidator<Int> {\n        override fun validate(value: Int): String? {\n            return when {\n                value < min -> \"Value must be at least $min\"\n                value > max -> \"Value must be at most $max\"\n                else -> null\n            }\n        }\n    }\n    \n    /**\n     * URL 验证器\n     */\n    fun url(): ConfigValidator<String> = object : ConfigValidator<String> {\n        override fun validate(value: String): String? {\n            return try {\n                java.net.URL(value)\n                null\n            } catch (e: Exception) {\n                \"Invalid URL format: $value\"\n            }\n        }\n    }\n    \n    /**\n     * 组合验证器（多个验证器同时生效）\n     */\n    fun <T> combine(vararg validators: ConfigValidator<T>): ConfigValidator<T> = object : ConfigValidator<T> {\n        override fun validate(value: T): String? {\n            validators.forEach { validator ->\n                validator.validate(value)?.let { return it }\n            }\n            return null\n        }\n    }\n    \n    /**\n     * 可选验证器（值为 null 时跳过验证）\n     */\n    fun <T> optional(validator: ConfigValidator<T>): ConfigValidator<T?> = object : ConfigValidator<T?> {\n        override fun validate(value: T?): String? {\n            return if (value == null) {\n                null\n            } else {\n                validator.validate(value)\n            }\n        }\n    }\n}\n\n"
  },
  {
    "path": "config/src/main/kotlin/cn/netdiscovery/monica/config/storage/ConfigManager.kt",
    "content": "package cn.netdiscovery.monica.config.storage\n\nimport com.safframework.rxcache.RxCache\nimport org.slf4j.Logger\nimport org.slf4j.LoggerFactory\n\n/**\n * 统一配置管理器\n * \n * 管理不同类型的配置存储，提供统一的配置访问接口。\n * 支持多种存储后端：\n * - RxCache: 用于复杂对象（如 GeneralSettings）\n * - Preferences: 用于简单键值对（如语言设置）\n * - File: 用于 JSON 配置文件（如滤镜参数元数据）\n * \n * 注意：需要先调用 initialize() 方法初始化，传入 RxCache 实例。\n * \n * @author: Tony Shen\n * @date: 2025-12-12\n */\nobject ConfigManager {\n    \n    private val logger: Logger = LoggerFactory.getLogger(ConfigManager::class.java)\n    \n    private var _rxCacheStorage: ConfigStorage? = null\n    \n    /**\n     * RxCache 存储（用于复杂对象）\n     */\n    val rxCacheStorage: ConfigStorage\n        get() = _rxCacheStorage ?: throw IllegalStateException(\"ConfigManager not initialized. Call initialize() first.\")\n    \n    /**\n     * Preferences 存储（用于简单键值对）\n     */\n    val preferencesStorage: ConfigStorage = PreferencesConfigStorage()\n    \n    /**\n     * 默认存储（优先使用 RxCache）\n     */\n    val defaultStorage: ConfigStorage\n        get() = rxCacheStorage\n    \n    /**\n     * 初始化 ConfigManager\n     * \n     * @param rxCache RxCache 实例\n     */\n    fun initialize(rxCache: RxCache) {\n        _rxCacheStorage = RxCacheConfigStorage(rxCache)\n        logger.info(\"ConfigManager initialized\")\n    }\n    \n    /**\n     * 根据配置类型选择合适的存储\n     * \n     * @param configType 配置类型\n     * @return 对应的存储实例\n     */\n    fun getStorage(configType: ConfigType = ConfigType.DEFAULT): ConfigStorage {\n        return when (configType) {\n            ConfigType.RX_CACHE -> rxCacheStorage\n            ConfigType.PREFERENCES -> preferencesStorage\n            ConfigType.DEFAULT -> defaultStorage\n        }\n    }\n    \n    /**\n     * 保存配置（使用默认存储）\n     */\n    fun <T> save(key: String, value: T, configType: ConfigType = ConfigType.DEFAULT) {\n        try {\n            getStorage(configType).save(key, value)\n            logger.debug(\"Config saved: key=$key, type=$configType\")\n        } catch (e: Exception) {\n            logger.error(\"Failed to save config: key=$key, type=$configType\", e)\n            throw e\n        }\n    }\n    \n    /**\n     * 加载配置（使用默认存储）\n     */\n    fun <T> load(key: String, default: T, configType: ConfigType = ConfigType.DEFAULT): T {\n        return try {\n            val value = getStorage(configType).load(key, default)\n            logger.debug(\"Config loaded: key=$key, type=$configType, found=${value != default}\")\n            value\n        } catch (e: Exception) {\n            logger.warn(\"Failed to load config: key=$key, type=$configType, using default\", e)\n            default\n        }\n    }\n    \n    /**\n     * 检查配置是否存在\n     */\n    fun exists(key: String, configType: ConfigType = ConfigType.DEFAULT): Boolean {\n        return getStorage(configType).exists(key)\n    }\n    \n    /**\n     * 删除配置\n     */\n    fun remove(key: String, configType: ConfigType = ConfigType.DEFAULT) {\n        try {\n            getStorage(configType).remove(key)\n            logger.debug(\"Config removed: key=$key, type=$configType\")\n        } catch (e: Exception) {\n            logger.error(\"Failed to remove config: key=$key, type=$configType\", e)\n            throw e\n        }\n    }\n    \n    /**\n     * 清空指定类型的配置\n     */\n    fun clear(configType: ConfigType = ConfigType.DEFAULT) {\n        try {\n            getStorage(configType).clear()\n            logger.info(\"Config cleared: type=$configType\")\n        } catch (e: Exception) {\n            logger.error(\"Failed to clear config: type=$configType\", e)\n            throw e\n        }\n    }\n    \n    /**\n     * 获取所有配置键\n     */\n    fun getAllKeys(configType: ConfigType = ConfigType.DEFAULT): List<String> {\n        return getStorage(configType).getAllKeys()\n    }\n}\n\n/**\n * 配置类型枚举\n */\nenum class ConfigType {\n    /**\n     * 使用 RxCache 存储（默认，用于复杂对象）\n     */\n    RX_CACHE,\n    \n    /**\n     * 使用 Preferences 存储（用于简单键值对）\n     */\n    PREFERENCES,\n    \n    /**\n     * 使用默认存储（当前为 RxCache）\n     */\n    DEFAULT\n}\n\n"
  },
  {
    "path": "config/src/main/kotlin/cn/netdiscovery/monica/config/storage/ConfigStorage.kt",
    "content": "package cn.netdiscovery.monica.config.storage\n\n/**\n * 统一配置存储接口\n * \n * 抽象了不同存储实现的差异，提供统一的配置读写接口。\n * \n * @author: Tony Shen\n * @date: 2025-12-12\n */\ninterface ConfigStorage {\n    /**\n     * 保存配置值\n     * \n     * @param key 配置键\n     * @param value 配置值（支持基本类型和可序列化对象）\n     */\n    fun <T> save(key: String, value: T)\n    \n    /**\n     * 加载配置值\n     * \n     * @param key 配置键\n     * @param default 默认值（当配置不存在时返回）\n     * @return 配置值，如果不存在则返回默认值\n     */\n    fun <T> load(key: String, default: T): T\n    \n    /**\n     * 检查配置是否存在\n     * \n     * @param key 配置键\n     * @return 如果配置存在返回 true，否则返回 false\n     */\n    fun exists(key: String): Boolean\n    \n    /**\n     * 删除配置\n     * \n     * @param key 配置键\n     */\n    fun remove(key: String)\n    \n    /**\n     * 清空所有配置\n     */\n    fun clear()\n    \n    /**\n     * 获取所有配置键\n     * \n     * @return 配置键列表\n     */\n    fun getAllKeys(): List<String>\n}\n\n"
  },
  {
    "path": "config/src/main/kotlin/cn/netdiscovery/monica/config/storage/FileConfigStorage.kt",
    "content": "package cn.netdiscovery.monica.config.storage\n\nimport com.google.gson.Gson\nimport com.google.gson.reflect.TypeToken\nimport org.slf4j.Logger\nimport org.slf4j.LoggerFactory\nimport java.io.File\nimport java.nio.file.Files\nimport java.nio.file.StandardOpenOption\n\n/**\n * 文件配置存储适配器（JSON 格式）\n * \n * 用于存储 JSON 格式的配置文件（如滤镜参数元数据）。\n * 所有配置存储在一个 JSON 文件中。\n * \n * @param configFile 配置文件路径\n * @param gson Gson 实例，用于序列化/反序列化\n * \n * @author: Tony Shen\n * @date: 2025-12-12\n */\nclass FileConfigStorage(\n    private val configFile: File,\n    private val gson: Gson = Gson()\n) : ConfigStorage {\n    \n    private val logger: Logger = LoggerFactory.getLogger(FileConfigStorage::class.java)\n    \n    private val configMap: MutableMap<String, Any> by lazy {\n        loadFromFile()\n    }\n    \n    /**\n     * 从文件加载配置\n     */\n    private fun loadFromFile(): MutableMap<String, Any> {\n        return if (configFile.exists() && configFile.isFile) {\n            try {\n                val jsonContent = configFile.readText(Charsets.UTF_8)\n                if (jsonContent.isBlank()) {\n                    mutableMapOf()\n                } else {\n                    val type = object : TypeToken<Map<String, Any>>() {}.type\n                    gson.fromJson(jsonContent, type) ?: mutableMapOf()\n                }\n            } catch (e: Exception) {\n                logger.error(\"Failed to load config from file: ${configFile.absolutePath}\", e)\n                mutableMapOf()\n            }\n        } else {\n            // 文件不存在，创建空配置\n            mutableMapOf()\n        }\n    }\n    \n    /**\n     * 保存配置到文件\n     */\n    private fun saveToFile() {\n        try {\n            // 确保父目录存在\n            configFile.parentFile?.mkdirs()\n            \n            val jsonContent = gson.toJson(configMap)\n            Files.write(\n                configFile.toPath(),\n                jsonContent.toByteArray(Charsets.UTF_8),\n                StandardOpenOption.CREATE,\n                StandardOpenOption.TRUNCATE_EXISTING,\n                StandardOpenOption.WRITE\n            )\n        } catch (e: Exception) {\n            logger.error(\"Failed to save config to file: ${configFile.absolutePath}\", e)\n            throw ConfigStorageException(\"Failed to save config to file\", e)\n        }\n    }\n    \n    override fun <T> save(key: String, value: T) {\n        try {\n            configMap[key] = value as Any\n            saveToFile()\n        } catch (e: Exception) {\n            logger.error(\"Failed to save config with key: $key\", e)\n            throw ConfigStorageException(\"Failed to save config: $key\", e)\n        }\n    }\n    \n    @Suppress(\"UNCHECKED_CAST\")\n    override fun <T> load(key: String, default: T): T {\n        return try {\n            val value = configMap[key]\n            if (value != null) {\n                // 尝试类型转换\n                when {\n                    default is String && value is String -> value as T\n                    default is Int && value is Number -> value.toInt() as T\n                    default is Long && value is Number -> value.toLong() as T\n                    default is Float && value is Number -> value.toFloat() as T\n                    default is Double && value is Number -> value.toDouble() as T\n                    default is Boolean && value is Boolean -> value as T\n                    else -> {\n                        // 尝试使用 Gson 进行类型转换\n                        val jsonValue = gson.toJson(value)\n                        gson.fromJson(jsonValue, default!!::class.java) as T\n                    }\n                }\n            } else {\n                default\n            }\n        } catch (e: Exception) {\n            logger.warn(\"Failed to load config with key: $key, using default value\", e)\n            default\n        }\n    }\n    \n    override fun exists(key: String): Boolean {\n        return configMap.containsKey(key)\n    }\n    \n    override fun remove(key: String) {\n        try {\n            configMap.remove(key)\n            saveToFile()\n        } catch (e: Exception) {\n            logger.error(\"Failed to remove config with key: $key\", e)\n            throw ConfigStorageException(\"Failed to remove config: $key\", e)\n        }\n    }\n    \n    override fun clear() {\n        try {\n            configMap.clear()\n            saveToFile()\n        } catch (e: Exception) {\n            logger.error(\"Failed to clear config storage\", e)\n            throw ConfigStorageException(\"Failed to clear config storage\", e)\n        }\n    }\n    \n    override fun getAllKeys(): List<String> {\n        return configMap.keys.toList()\n    }\n}"
  },
  {
    "path": "config/src/main/kotlin/cn/netdiscovery/monica/config/storage/PreferencesConfigStorage.kt",
    "content": "package cn.netdiscovery.monica.config.storage\n\nimport org.slf4j.Logger\nimport org.slf4j.LoggerFactory\nimport java.util.prefs.Preferences\n\n/**\n * Preferences 配置存储适配器\n * \n * 适配 Java Preferences API，用于存储简单的键值对配置（如语言设置）。\n * \n * @param preferencesNode Preferences 节点，默认为用户节点\n * \n * @author: Tony Shen\n * @date: 2025-12-12\n */\nclass PreferencesConfigStorage(\n    private val preferencesNode: Preferences = Preferences.userNodeForPackage(PreferencesConfigStorage::class.java)\n) : ConfigStorage {\n    \n    private val logger: Logger = LoggerFactory.getLogger(PreferencesConfigStorage::class.java)\n    \n    override fun <T> save(key: String, value: T) {\n        try {\n            when (value) {\n                is String -> preferencesNode.put(key, value)\n                is Int -> preferencesNode.putInt(key, value)\n                is Long -> preferencesNode.putLong(key, value)\n                is Float -> preferencesNode.putFloat(key, value)\n                is Double -> preferencesNode.putDouble(key, value)\n                is Boolean -> preferencesNode.putBoolean(key, value)\n                is ByteArray -> preferencesNode.putByteArray(key, value)\n                else -> {\n                    // 对于复杂对象，序列化为 JSON 字符串\n                    preferencesNode.put(key, value.toString())\n                    logger.warn(\"Complex object serialized as string for key: $key\")\n                }\n            }\n            preferencesNode.flush()\n        } catch (e: Exception) {\n            logger.error(\"Failed to save config with key: $key\", e)\n            throw ConfigStorageException(\"Failed to save config: $key\", e)\n        }\n    }\n    \n    @Suppress(\"UNCHECKED_CAST\")\n    override fun <T> load(key: String, default: T): T {\n        return try {\n            when (default) {\n                is String -> preferencesNode.get(key, default) as T\n                is Int -> preferencesNode.getInt(key, default) as T\n                is Long -> preferencesNode.getLong(key, default) as T\n                is Float -> preferencesNode.getFloat(key, default) as T\n                is Double -> preferencesNode.getDouble(key, default) as T\n                is Boolean -> preferencesNode.getBoolean(key, default) as T\n                is ByteArray -> preferencesNode.getByteArray(key, default) as T\n                else -> {\n                    val value = preferencesNode.get(key, null)\n                    if (value != null) {\n                        // 尝试从字符串反序列化（需要类型信息）\n                        logger.warn(\"Complex object deserialization not fully supported for key: $key, returning default\")\n                        default\n                    } else {\n                        default\n                    }\n                }\n            }\n        } catch (e: Exception) {\n            logger.warn(\"Failed to load config with key: $key, using default value\", e)\n            default\n        }\n    }\n    \n    override fun exists(key: String): Boolean {\n        return try {\n            preferencesNode.get(key, null) != null\n        } catch (e: Exception) {\n            logger.warn(\"Failed to check existence of config with key: $key\", e)\n            false\n        }\n    }\n    \n    override fun remove(key: String) {\n        try {\n            preferencesNode.remove(key)\n            preferencesNode.flush()\n        } catch (e: Exception) {\n            logger.error(\"Failed to remove config with key: $key\", e)\n            throw ConfigStorageException(\"Failed to remove config: $key\", e)\n        }\n    }\n    \n    override fun clear() {\n        try {\n            preferencesNode.clear()\n            preferencesNode.flush()\n        } catch (e: Exception) {\n            logger.error(\"Failed to clear Preferences\", e)\n            throw ConfigStorageException(\"Failed to clear config storage\", e)\n        }\n    }\n    \n    override fun getAllKeys(): List<String> {\n        return try {\n            preferencesNode.keys().toList()\n        } catch (e: Exception) {\n            logger.error(\"Failed to get all keys from Preferences\", e)\n            emptyList()\n        }\n    }\n}\n\n"
  },
  {
    "path": "config/src/main/kotlin/cn/netdiscovery/monica/config/storage/RxCacheConfigStorage.kt",
    "content": "package cn.netdiscovery.monica.config.storage\n\nimport com.google.gson.Gson\nimport com.google.gson.reflect.TypeToken\nimport com.safframework.rxcache.RxCache\nimport com.safframework.rxcache.ext.get\nimport org.slf4j.Logger\nimport org.slf4j.LoggerFactory\n\n/**\n * RxCache 配置存储适配器\n * \n * 适配现有的 RxCache 实现，用于存储复杂对象（如 GeneralSettings）。\n * \n * 注意：由于 RxCache 的 get 方法需要 reified 类型参数，我们使用 Any 类型进行通用处理。\n * 对于类型安全的场景，建议使用具体的类型调用。\n * \n * @param rxCache RxCache 实例，由外部传入以避免循环依赖\n * \n * @author: Tony Shen\n * @date: 2025-12-12\n */\nclass RxCacheConfigStorage(\n    private val rxCache: RxCache\n) : ConfigStorage {\n    \n    private val logger: Logger = LoggerFactory.getLogger(RxCacheConfigStorage::class.java)\n    private val gson = Gson()\n    \n    override fun <T> save(key: String, value: T) {\n        try {\n            rxCache.saveOrUpdate(key, value)\n        } catch (e: Exception) {\n            logger.error(\"Failed to save config with key: $key\", e)\n            throw ConfigStorageException(\"Failed to save config: $key\", e)\n        }\n    }\n    \n    @Suppress(\"UNCHECKED_CAST\")\n    override fun <T> load(key: String, default: T): T {\n        return try {\n            // RxCache.get 需要 reified 类型参数，这里使用 Any 作为通用类型\n            // 实际使用时，类型转换由调用方保证（通过 default 参数的类型推断）\n            val result = rxCache.get<Any>(key)?.data\n            if (result != null) {\n                // 尝试类型转换\n                // 对于基本类型，进行显式转换\n                when {\n                    default is String && result is String -> result as T\n                    default is Int && result is Number -> result.toInt() as T\n                    default is Long && result is Number -> result.toLong() as T\n                    default is Float && result is Number -> result.toFloat() as T\n                    default is Double && result is Number -> result.toDouble() as T\n                    default is Boolean && result is Boolean -> result as T\n                    // 对于复杂对象，检查类型是否匹配\n                    result::class.java.isAssignableFrom(default!!::class.java) -> result as T\n                    default::class.java.isAssignableFrom(result::class.java) -> result as T\n                    // 如果 result 是 LinkedTreeMap（Gson 反序列化的结果），尝试用 Gson 转换\n                    result is com.google.gson.internal.LinkedTreeMap<*, *> -> {\n                        try {\n                            val json = gson.toJson(result)\n                            @Suppress(\"UNCHECKED_CAST\")\n                            gson.fromJson(json, default!!::class.java) as T ?: default\n                        } catch (e: Exception) {\n                            logger.warn(\"Failed to convert LinkedTreeMap to ${default!!::class.java.simpleName} for key: $key\", e)\n                            default\n                        }\n                    }\n                    else -> {\n                        logger.warn(\"Type mismatch for key: $key, expected: ${default!!::class.java.simpleName}, got: ${result::class.java.simpleName}\")\n                        default\n                    }\n                }\n            } else {\n                default\n            }\n        } catch (e: Exception) {\n            logger.warn(\"Failed to load config with key: $key, using default value\", e)\n            default\n        }\n    }\n    \n    override fun exists(key: String): Boolean {\n        return try {\n            rxCache.get<Any>(key) != null\n        } catch (e: Exception) {\n            logger.warn(\"Failed to check existence of config with key: $key\", e)\n            false\n        }\n    }\n    \n    override fun remove(key: String) {\n        try {\n            rxCache.remove(key)\n        } catch (e: Exception) {\n            logger.error(\"Failed to remove config with key: $key\", e)\n            throw ConfigStorageException(\"Failed to remove config: $key\", e)\n        }\n    }\n    \n    override fun clear() {\n        try {\n            rxCache.clear()\n        } catch (e: Exception) {\n            logger.error(\"Failed to clear RxCache\", e)\n            throw ConfigStorageException(\"Failed to clear config storage\", e)\n        }\n    }\n    \n    override fun getAllKeys(): List<String> {\n        // RxCache 不直接提供获取所有键的接口，返回空列表\n        // 如果需要此功能，可以考虑维护一个键列表\n        logger.warn(\"RxCache does not support getAllKeys(), returning empty list\")\n        return emptyList()\n    }\n}\n\n/**\n * 配置存储异常\n */\nclass ConfigStorageException(message: String, cause: Throwable? = null) : Exception(message, cause)\n\n"
  },
  {
    "path": "docs/filter_module_refactor.md",
    "content": "# 滤镜模块 UI 重构与优化说明（2025-12）\n\n## 背景与目标\n\n本次重构的核心目标：\n\n- **提升 UI 可用性与一致性**：对齐、间距、状态提示更清晰，符合图像编辑软件的交互预期。\n- **保持业务逻辑不变/可控演进**：在不破坏滤镜算法实现的前提下，整理 UI 状态与交互。\n- **提升性能与稳定性**：拖动体验更顺滑，避免频繁计算与 CPU 抖动；修复已知崩溃点与错位问题。\n\n---\n\n## 关键交互语义（最终形态）\n\n### 1）拖动即提交（去掉 Apply 按钮）\n\n- **滤镜选择**：点击某个滤镜后，立即在编辑器画布上应用一次（并记录一次历史）。\n- **参数调整**：\n  - 拖动过程中：仍会以 **300ms 抽样**方式触发预览（降低计算频率）。\n  - 松手后：**立即提交**到编辑器画布（并记录一次历史），避免多次历史碎片化。\n  - 文本输入：输入过程只做预览；按 `Done` 后提交一次。\n\n> 说明：提交时使用“进入滤镜模块前的基线图”作为输入，避免在 `currentImage` 上反复叠加导致效果漂移。\n\n### 2）Reset / Cancel / 清除滤镜\n\n- **Reset（重置滤镜）**：恢复当前滤镜的默认参数，并立即提交一次（记录历史）。\n- **清除滤镜**：恢复到进入滤镜模块前的效果（基线图），记录一次历史，并取消滤镜选中态。\n- **Cancel**：用于取消未提交的预览态（例如仅有 previewImage），回到上次提交参数快照并清理预览。\n\n---\n\n## UI 结构拆分与状态管理\n\n### 1）去全局状态（多实例安全）\n\n移除文件级全局变量 `filterSelectedIndex` / `filterTempMap`，改为在 `filter()` 内使用 `remember` 状态：\n\n- `selectedIndexState`\n- `paramMap`（`mutableStateMapOf`）\n- `appliedParamSnapshot`\n- `baseImageSnapshot`（进入模块前基线图）\n\n避免了多窗口/多次进入模块时状态串扰的问题。\n\n### 2）右侧面板底栏固定\n\n修复了右侧面板滚动区域占满高度导致底部按钮区域不可见的问题：滚动区使用 `weight(1f)`，底栏固定展示。\n\n### 3）收起时参数摘要\n\n当参数区收起时，展示“参数摘要”卡片：\n\n- 默认/已调整项数量\n- 展示部分差异项\n- Reset 提示（引导用户使用底部按钮）\n\n---\n\n## 参数范围与格式化（配置化）\n\n新增参数 UI 元信息：\n\n- `FilterParamMeta(min, max, step, decimals)`\n- `FilterParamMetaRegistry.resolve(filterName, param)`：统一解析范围、步长、显示小数位。\n\n并新增默认参数构建工具：\n\n- `buildDefaultParamMap(filterName)`：用于初始化/Reset/判断是否处于默认参数状态。\n- Float/Double 默认值按 `decimals` 统一格式化，避免 UI 显示不一致。\n\n### BlockFilter 安全修复（step=0 崩溃）\n\n问题：`BlockFilter` 内部将 `blockSize` 用于 `range.step(blockSize)`，当 `blockSize=0` 会直接抛异常。\n\n修复策略（三道防线）：\n\n1. `FilterParamMetaRegistry` 为 `blockSize` 设置 `min=1`。\n2. 默认参数构建时按 meta.min 对 Int 进行 clamp（即使缓存里有 0 也会被纠正）。\n3. `BlockFilter` 构造函数内防御性修复：`max(1, blockSize)`，彻底杜绝 crash。\n\n---\n\n## 性能优化\n\n### 1）Slider 抽样预览（300ms）+ 松手提交\n\n拖动过程中不实时提交，降低重算频率；松手后一次性提交，历史更干净。\n\n### 2）预览缓存（同滤镜 + 同参数 hash 命中）\n\n在 `FilterViewModel.applyFilterPreview()` 中引入 LRU 预览缓存：\n\n- Key：`baseImageId(identityHashCode) + filterName + paramsHash(稳定排序后 hash)`\n- 策略：LRU + 双阈值淘汰（条目数 + 估算内存上限）\n- `clear()` 时会清空缓存，避免跨页面持有内存\n\n收益：重复参数回退/来回拖动时命中缓存，CPU 更稳、预览更顺滑。\n\n---\n\n## Bug 修复汇总\n\n- **搜索列表点击/选中错位**：修复 `itemsIndexed` 使用位置 index 误当真实 filterIndex 的问题。\n- **英文硬编码**：如 `No Image` 等占位文案改为 i18n。\n- **右侧按钮不显示**：滚动区域 `fillMaxSize()` 挤掉底栏的问题修复为 `weight(1f)`。\n- **BlockFilter step=0 崩溃**：如上“三道防线”修复，并处理缓存里持久化为 0 的脏数据。\n\n---\n\n## 国际化（i18n）新增/补充 Key（节选）\n\n- `no_image` / `no_filters_found`\n- `param_summary` / `param_summary_default` / `param_summary_changed_count` / `param_summary_reset_hint`\n- `clear_filter`\n\n---\n\n## 涉及文件清单（主要）\n\n- `src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/filter/FilterView.kt`\n- `src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/filter/FilterListPanel.kt`\n- `src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/filter/FilterPreviewArea.kt`\n- `src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/filter/FilterAdjustmentPanel.kt`\n- `src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/filter/FilterViewModel.kt`\n- `src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/filter/FilterParamMeta.kt`\n- `src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/filter/FilterParamDefaults.kt`\n- `imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/filter/BlockFilter.kt`\n- `i18n/src/main/resources/strings/strings_zh.xml`\n- `i18n/src/main/resources/strings/strings_en.xml`\n\n---\n\n## 已知未完成项 / 后续建议\n\n- **导出功能**：`FilterTopAppBar` 的 Export 仍为 TODO。\n- **日志与可观测性**：当前仍存在少量 `logger.info(...)`（如 FilterView 生命周期/FilterViewModel applyFilter）。建议后续统一降噪或增加 debug 开关。\n- **可访问性**：个别 `contentDescription` 仍为英文（如 Zoom In），可按 i18n 统一。\n\n---\n\n## 未来优化方向（路线图建议）\n\n### P0（高收益 / 低风险，建议优先）\n\n- **提交任务的并发与取消策略**  \n  - 当前“松手即提交”在用户频繁操作时可能产生提交排队；建议在提交前取消上一次未完成的提交任务，仅保留最后一次松手的提交（类似“last-write-wins”）。\n  - 预览任务与提交任务建议分别管理，避免互相 cancel 造成 UI 抖动。\n\n- **预览缓存进一步完善**\n  - 目前缓存 key 基于 `identityHashCode(baseImage)`；若后续引入“滤镜叠加链”，可扩展为 `baseImageFingerprint + chainHash + paramsHash`（或至少在 sourceImageOverride 场景下保证 cacheKey 取对）。\n  - 可增加简单命中率统计（默认关闭），便于性能回归。\n\n- **枚举型参数的系统化支持**\n  - 已对 `ColorFilter.style`、`NatureFilter.style` 做了下拉选择；建议把更多类似参数（如 `gridType`、`waveType` 等）统一纳入 `FilterParamMetaRegistry` 的 `enumOptions`。\n  - 枚举项建议改为外部配置（json），降低 Kotlin 侧维护成本。\n\n### P1（高收益 / 中等工程量）\n\n- **非破坏式滤镜栈（专业编辑器体验）**\n  - 当前“方式1”支持滤镜叠加，但本质是“破坏式”写回 `currentImage`；建议升级为滤镜栈（A→B→C）：\n    - UI：支持新增/删除/排序/启用/禁用滤镜条目；\n    - 计算：以基线图为输入重算整条链（可配合分段缓存）；\n    - 历史：一次“应用/确认”生成一个历史节点，或者按滤镜条目粒度记录。\n  - 优点：可编辑、可回溯、符合 PS/Lightroom 预期；缺点：需要明确栈的存储与性能策略。\n\n- **参数元数据完全配置化**\n  - 将 `FilterParamMeta`（min/max/step/decimals/enumOptions）迁移到 `resources/common/filterParamMeta.json`（或扩展现有 `filterConfig.json`），Kotlin 只保留类型默认兜底与少量安全约束（例如 step>0）。\n  - 这样可以由产品/算法侧直接调整范围与枚举定义，不需要改代码。\n\n- **提交与撤销语义更精确**\n  - 当前每次松手都会 push 历史；可考虑“合并提交窗口”（例如 500ms 内多次提交合并为一次历史），减少 undo 栈污染。\n  - 也可增加“预览模式”开关：只预览不入历史，用户确认后再统一落盘。\n\n### P2（体验增强 / 可持续维护）\n\n- **导出能力落地（与滤镜结果一致）**\n  - Export 需要明确导出的是：当前画布效果（含叠加）还是仅某个滤镜结果。\n  - 建议支持：导出当前效果 / 导出原图 / 导出带滤镜栈元数据（用于二次编辑）。\n\n- **测试与回归保障**\n  - 为关键交互补充自动化验证：列表筛选点击不乱、BlockFilter step 不为 0、枚举下拉可用、缓存命中不串图、清除滤镜恢复正确。\n\n- **可访问性与键盘操作**\n  - 下拉/按钮/缩放控件补齐 i18n 的 `contentDescription`；\n  - 为参数控件支持键盘上下调整与快捷键（更像桌面编辑器）。\n\n\n"
  },
  {
    "path": "docs/layer_render_cache_analysis.md",
    "content": "# 图层渲染缓存优化分析\n\n## 当前实现分析\n\n### 1. 渲染流程\n- `CanvasView` 使用 `collectAsState()` 观察图层列表\n- 每次图层变化都会触发 Canvas 重组和重绘\n- `LayerRenderer.drawAll()` 遍历所有图层并调用 `render()`\n- 每个图层都使用 `drawIntoCanvas` + `saveLayer`，有性能开销\n\n### 2. 性能瓶颈\n- **ImageLayer**: 每次重绘都重新计算变换（平移、旋转、缩放）\n- **ShapeLayer**: 每次重绘都重新绘制所有形状\n- **Canvas 重组**: 图层列表变化时，整个 Canvas 都会重组\n\n## 优化方案对比\n\n### 方案一：路线图中的 ImageBitmap 缓存（不推荐）\n\n**问题**：\n1. Compose 的 `DrawScope` 在每次重组时都会重新创建，不能直接缓存 `ImageBitmap`\n2. 缓存需要考虑画布尺寸变化（Canvas 尺寸可能变化）\n3. 需要考虑透明度、变换等属性的变化\n4. 缓存失效逻辑复杂（何时清除缓存？）\n\n**适用场景**：\n- 静态图像，尺寸固定\n- 不适用于动态变化的图层\n\n### 方案二：Compose 级别的缓存（推荐）⭐\n\n**核心思路**：\n1. 使用 `remember` + `key()` 为每个图层创建独立的缓存\n2. 使用 `Modifier.drawWithCache` 缓存绘制内容\n3. 使用版本号/哈希值标记图层变化\n\n**优势**：\n- 利用 Compose 的缓存机制，自动管理生命周期\n- 画布尺寸变化时自动失效\n- 代码简洁，易于维护\n\n**实现要点**：\n```kotlin\n@Composable\nfun LayerRenderer(\n    layers: List<Layer>,\n    canvasSize: Size\n) {\n    layers.forEach { layer ->\n        key(layer.id, layer.version) { // 版本号标记变化\n            DrawScope.drawWithCache {\n                // 缓存绘制内容\n                onDrawBehind {\n                    layer.render(this)\n                }\n            }\n        }\n    }\n}\n```\n\n### 方案三：分层缓存策略（最佳）⭐⭐\n\n**核心思路**：\n1. **ImageLayer**: 缓存变换后的图像（使用 `drawWithCache`）\n2. **ShapeLayer**: 使用 `remember` 缓存形状列表，避免重复计算\n3. **组合优化**: 只重绘变化的图层区域\n\n**实现要点**：\n\n#### 1. Layer 基类添加版本号\n```kotlin\nabstract class Layer {\n    private var _version by mutableStateOf(0L)\n    val version: Long get() = _version\n    \n    protected fun markDirty() {\n        _version++\n    }\n    \n    // 属性变化时调用\n    fun updateOpacity(alpha: Float) {\n        opacity = alpha.coerceIn(0f, 1f)\n        markDirty()\n    }\n}\n```\n\n#### 2. ImageLayer 缓存变换结果\n```kotlin\n@Composable\nfun ImageLayerRenderer(\n    layer: ImageLayer,\n    canvasSize: Size\n) {\n    val cachedImage = remember(layer.id, layer.version, canvasSize) {\n        // 计算变换后的图像\n        renderToBitmap(layer, canvasSize)\n    }\n    \n    Canvas(modifier = Modifier.drawWithCache {\n        onDrawBehind {\n            drawImage(cachedImage)\n        }\n    })\n}\n```\n\n#### 3. ShapeLayer 缓存形状列表\n```kotlin\n@Composable\nfun ShapeLayerRenderer(\n    layer: ShapeLayer,\n    canvasSize: Size\n) {\n    val shapes = remember(layer.id, layer.version) {\n        // 缓存形状列表，避免重复计算\n        layer.getAllShapes()\n    }\n    \n    Canvas(modifier = Modifier) {\n        shapes.forEach { shape ->\n            drawShape(shape)\n        }\n    }\n}\n```\n\n#### 4. LayerRenderer 优化\n```kotlin\nclass LayerRenderer {\n    fun drawAll(drawScope: DrawScope, layers: List<Layer>) {\n        layers.forEach { layer ->\n            if (!layer.visible || layer.opacity <= 0f) return@forEach\n            \n            // 使用 key 确保只有变化的图层才重绘\n            key(layer.id, layer.version) {\n                drawLayer(drawScope, layer)\n            }\n        }\n    }\n}\n```\n\n## 性能提升预估\n\n### 当前性能\n- 10 个图层，每次重绘耗时：~50ms\n- 拖动图像层时：~16ms/frame（60fps 可能卡顿）\n\n### 优化后性能\n- 10 个图层，未变化时：~5ms（使用缓存）\n- 拖动图像层时：~8ms/frame（只重绘变化的图层）\n\n**提升**：约 5-10 倍性能提升\n\n## 实施建议\n\n### 阶段一：基础优化（2-3 天）\n1. 在 `Layer` 基类添加 `version` 字段\n2. 属性变化时调用 `markDirty()`\n3. 使用 `key()` 优化 Compose 重组\n\n### 阶段二：ImageLayer 缓存（2-3 天）\n1. 使用 `remember` 缓存变换后的图像\n2. 使用 `Modifier.drawWithCache` 缓存绘制内容\n3. 处理画布尺寸变化\n\n### 阶段三：ShapeLayer 优化（1-2 天）\n1. 缓存形状列表\n2. 优化形状绘制逻辑\n\n### 阶段四：增量渲染（可选，3-5 天）\n1. 只重绘变化的图层区域\n2. 使用脏矩形技术\n\n## 注意事项\n\n1. **内存管理**：\n   - 缓存会占用内存，需要设置上限\n   - 使用 `SoftReference` 或 LRU 缓存\n\n2. **缓存失效**：\n   - 画布尺寸变化时自动失效\n   - 图层属性变化时手动失效\n\n3. **兼容性**：\n   - 确保与现有代码兼容\n   - 不影响导出功能\n\n4. **测试**：\n   - 测试大量图层场景\n   - 测试频繁变化场景\n   - 测试内存使用情况\n\n## 结论\n\n**推荐方案**：方案三（分层缓存策略）\n- 性能提升明显\n- 实现相对简单\n- 易于维护和扩展\n- 符合 Compose 最佳实践\n\n**预计工作量**：5-7 天（与路线图一致）\n\n**优先级**：中优先级（当前性能可接受，但优化后体验更好）\n\n\n\n\n\n\n"
  },
  {
    "path": "docs/layer_system.md",
    "content": "# 图层系统概览\n\n本文档记录 Monica 图层系统的最新实现，用于指导后续的功能扩展与维护。\n\n## 核心目标\n\n- 支持图像层与形状层的叠加管理，便于多图层编辑。\n- 统一渲染与导出流程，避免重复绘制逻辑。\n- 提供直观的 UI 面板，用于图层的增删、排序、锁定与重命名。\n\n## 主要模块\n\n| 模块 | 关键文件 | 功能 |\n| --- | --- | --- |\n| 图层抽象 | `ui/controlpanel/shapedrawing/layer/Layer.kt` | 统一的图层基类，封装名称、可见性、透明度、锁定状态等属性。 |\n| 图层管理 | `ui/controlpanel/shapedrawing/layer/LayerManager.kt` | 负责图层增删改查、排序、激活状态同步，提供监听机制。使用 `StateFlow` 实现响应式更新。 |\n| 图像层 | `ui/controlpanel/shapedrawing/layer/ImageLayer.kt` | 保存背景位图及平移、缩放、旋转等变换信息。支持自动适应画布并居中显示。 |\n| 形状层 | `ui/controlpanel/shapedrawing/layer/ShapeLayer.kt` | 承载形状绘制数据（线段、矩形、多边形、文本等）。当前限制最多创建 1 个形状层。 |\n| 渲染器 | `ui/controlpanel/shapedrawing/layer/LayerRenderer.kt` | 顺序遍历图层并绘制到 Compose `DrawScope`，支持透明度合成。 |\n| 控制器 | `ui/controlpanel/shapedrawing/EditorController.kt` | 整合管理器、渲染器、导出流程，并暴露工具切换、图层同步接口。限制形状层数量为 1。导出逻辑内联在控制器中，提供 `exportImageBitmap()` 和 `exportBufferedImage()` 方法。 |\n\n## 工作流\n\n```\n用户交互 → EditorController → LayerManager → LayerRenderer → Canvas\n                                 ↓\n                          导出方法（内联在 EditorController 中）\n```\n\n1. UI 侧（例如 `ShapeDrawingView`）通过 `EditorController` 获取或创建图层。\n2. 用户绘制的形状实时写入当前激活的 `ShapeLayer`。\n3. `CanvasView`（`ui/controlpanel/shapedrawing/widget/CanvasView.kt`）调用 `LayerRenderer.drawAll()` 依次绘制每个图层，并根据透明度应用 `saveLayer`。\n4. 导出功能复用渲染器，将所有图层合成为位图或 AWT 图像。\n\n## UI 面板\n\n文件：`ui/controlpanel/shapedrawing/widget/LayerPanel.kt`\n\n- 左侧卡片式列表展示所有图层（顶部为最新图层）。\n- 支持：\n  - 可见性勾选\n  - 锁定/解锁（锁定后图标变红）\n  - 重命名（内联编辑）\n  - 上移/下移排序\n  - 新建形状层（按钮显示\"已达上限\"当达到限制时）\n  - 新建图像层\n- 激活的图层使用浅色高亮和边框，提供即时视觉反馈。\n- 当前激活的形状层显示\"• 当前绘制\"标识。\n\n## 关键交互\n\n- **初始化背景层**：`ShapeDrawingView`（`ui/controlpanel/shapedrawing/ShapeDrawingView.kt`）在载入图像时，通过 `LaunchedEffect(imageBitmap)` 从 `LayerManager` 中查找名为\"背景图层\"的图层，如果不存在则创建，如果存在则更新图像。确保状态同步，避免使用本地状态变量。\n- **形状写入**：每次拖动事件结束后，调用 `EditorController.replaceShapesInActiveLayer` 更新层数据。如果形状层已锁定，则禁止写入。在 `onDrag` 和 `onDragEnd` 中都会调用 `syncShapeLayer()` 同步形状数据。\n- **图像层拖动**：当激活图层为图像层且未锁定时，可以直接拖动图像层调整位置。拖动时更新 `LayerTransform.translation`，该变换会在自动适应和居中之后应用。\n- **导出**：点击保存按钮时，使用 `EditorController.exportBufferedImage` 获取合成结果。导出时使用显示尺寸（`ImageSizeCalculator.getImageDisplayPixelSize`，位于 `ui/widget/image/ImageSizeCalculator.kt`），并考虑 Canvas padding（8.dp），确保导出结果与显示效果一致。\n\n## 设计决策\n\n### 形状层限制\n- **限制数量**：当前实现限制最多创建 1 个形状层（`MAX_SHAPE_LAYERS = 1`），简化设计，避免多形状层带来的复杂性。\n- **图像层无限制**：支持创建多个图像层，每个图像层可以独立拖动和变换。\n\n### 背景层识别\n- **识别方式**：通过图层名称 `\"背景图层\"` 来识别背景层，而不是通过图像尺寸或其他属性。这种方式更可靠，不受图像尺寸变化影响。\n- **渲染差异**：\n  - 背景层：只应用自动适应和居中（`fitScale` 和 `centerOffset`），不应用用户定义的变换（`transform.translation`、`transform.rotation`、`transform.scaleX/Y`）。\n  - 用户添加的图像层：先应用自动适应和居中，再应用用户定义的变换。这样可以确保图像层在自动适应后，用户还可以进一步调整位置、旋转和缩放。\n\n### 坐标系统\n- **统一坐标**：使用显示尺寸（`ImageSizeCalculator.getImageDisplayPixelSize`，位于 `ui/widget/image/ImageSizeCalculator.kt`）作为坐标基准，确保绘制、显示和导出的一致性。导出时也会使用相同的显示尺寸，并减去 Canvas padding（8.dp × 2 = 16.dp），确保导出结果与显示效果完全一致。\n- **坐标转换器**：`CoordinateConverter`（`ui/controlpanel/shapedrawing/coordinate/CoordinateConverter.kt`）通过 `remember(state.currentImage, density.density)` 创建，当图像或密度变化时会自动重新计算转换比例，确保坐标转换的准确性。\n\n### 安全保护\n- **除零保护**：`ImageLayer.render()` 中在计算缩放比例前检查 `bitmap.width`、`bitmap.height`、`canvasWidth`、`canvasHeight` 是否大于 0，如果任一值为 0 或负数则直接返回，防止除零错误。\n- **锁定检查**：\n  - 在 `EditorController.addShapeToActiveLayer` 和 `replaceShapesInActiveLayer` 中检查形状层是否锁定，锁定状态下禁止修改。\n  - 在 `EditorController.canDrawOnActiveShapeLayer()` 中检查当前激活的形状层是否锁定，用于 UI 交互前的验证。\n  - 在 `ShapeDrawingView` 的拖动事件处理中，如果形状层已锁定，会显示提示并阻止绘制操作。\n\n## 测试覆盖\n\n| 测试文件 | 覆盖点 |\n| --- | --- |\n| `src/jvmTest/kotlin/cn/netdiscovery/monica/editor/layer/LayerManagerTest.kt` | 图层添加、激活同步、排序、清空等行为。 |\n| `src/jvmTest/kotlin/cn/netdiscovery/monica/editor/layer/ExportManagerTest.kt` | 图像层合成正确性（导出功能测试，类名为 `EditorControllerExportTest`）。 |\n\n> 当前测试依赖 `kotlin(\"test\")`，位于 `build.gradle.kts` 的 `jvmTest` SourceSet 中。\n\n## 已知问题与修复\n\n### 已修复的问题\n\n1. **除零错误保护**（2024-12）\n   - 问题：`ImageLayer.render()` 在 `bitmap.width` 或 `bitmap.height` 为 0 时可能发生除零错误。\n   - 修复：添加安全检查，在渲染前验证尺寸有效性。\n\n2. **坐标转换器不更新**（2024-12）\n   - 问题：`CoordinateConverter` 使用 `remember` 无依赖项，图像尺寸变化时不更新。\n   - 修复：添加 `state.currentImage` 和 `density.density` 作为依赖项。\n\n3. **背景层状态同步**（2024-12）\n   - 问题：使用本地 `backgroundLayer` 状态变量（`remember { mutableStateOf<ImageLayer?>(null) }`），与 `LayerManager` 不同步。如果用户通过其他方式修改了背景层，本地状态不会更新。\n   - 修复：移除了本地状态变量，改为在 `LaunchedEffect(imageBitmap)` 中直接从 `LayerManager.layers.value` 查找背景层，确保状态一致性。\n\n4. **导出尺寸不一致**（2024-12）\n   - 问题：导出时使用原始像素尺寸，与显示尺寸不一致。\n   - 修复：导出时使用显示尺寸，并考虑 Canvas padding（8.dp），确保导出结果与显示效果一致。\n\n## 后续待办\n\n- 形状层透明度、混合模式等高级属性。\n- 图层拖拽排序（UI 交互层面，当前仅支持上移/下移按钮）。\n- 控制器与其它工具模块（涂鸦、滤镜等）的整合策略。\n- 渲染性能评估与缓存机制。\n- 背景层删除保护（如果未来在 `LayerPanel` 中添加删除功能，需要防止删除背景层）。\n- 图像层的旋转和缩放交互（当前仅支持拖动位置）。\n\n> 📋 **详细优化路线图**: 请参考 [图层系统优化路线图](./layer_system_optimization_roadmap.md) 获取完整的优化计划、实施细节和时间估算。\n\n如需扩展新的图层类型，建议：\n\n1. 新建 `Layer` 子类，实现数据结构与 `render()`。\n2. 在 `LayerRenderer` 中添加对应的绘制分支。\n3. 在 `LayerPanel` 中增加图标、操作项。\n4. 补充单元测试覆盖新增逻辑。\n\n\n\n"
  },
  {
    "path": "docs/layer_system_optimization_roadmap.md",
    "content": "# 图层系统优化路线图\n\n本文档记录 Monica 图层系统的优化方向和实施计划，用于指导后续的功能扩展与性能提升。\n\n**文档版本**: 1.0  \n**最后更新**: 2024-12  \n**维护者**: Monica 开发团队\n\n---\n\n## 📋 目录\n\n- [一、功能扩展](#一功能扩展)\n- [二、性能优化](#二性能优化)\n- [三、代码质量提升](#三代码质量提升)\n- [四、架构优化](#四架构优化)\n- [五、用户体验改进](#五用户体验改进)\n- [六、实施计划](#六实施计划)\n- [七、技术债务清理](#七技术债务清理)\n- [八、监控与评估](#八监控与评估)\n\n---\n\n## 一、功能扩展\n\n### 1.1 UI 交互增强\n\n#### 1.1.1 图层删除功能 ⭐ 高优先级\n**当前状态**: `LayerPanel` 中没有删除按钮\n\n**目标**:\n- 在图层卡片中添加删除按钮（垃圾桶图标）\n- 删除前显示确认对话框\n- 防止误删除重要图层\n\n**实施要点**:\n```kotlin\n// 在 LayerPanel.kt 中添加删除按钮\nIconButton(\n    onClick = {\n        if (layer.name == \"背景图层\") {\n            state.showTray(\"无法删除背景图层\", \"提示\")\n        } else {\n            // 显示确认对话框\n            showDeleteConfirmDialog = true\n        }\n    }\n) {\n    Icon(Icons.Default.Delete, \"删除图层\")\n}\n```\n\n**技术细节**:\n- 添加背景层删除保护机制\n- 删除后自动激活上一个图层\n- 支持撤销删除（如果实现撤销/重做功能）\n\n**预计工作量**: 2-3 天\n\n---\n\n#### 1.1.2 拖拽排序 ⭐ 高优先级\n**当前状态**: 仅支持上移/下移按钮\n\n**目标**:\n- 实现图层卡片拖拽排序\n- 提供更直观的交互体验\n\n**实施要点**:\n```kotlin\n// 使用 Compose 的拖拽 API\nModifier\n    .pointerInput(Unit) {\n        detectDragGestures { change, dragAmount ->\n            // 处理拖拽逻辑\n        }\n    }\n```\n\n**技术细节**:\n- 使用 `Modifier.draggable()` 或 `Modifier.pointerInput()` 实现拖拽\n- 拖拽时显示视觉反馈（高亮、阴影）\n- 拖拽结束后更新图层顺序\n\n**预计工作量**: 3-5 天\n\n---\n\n#### 1.1.3 图层缩略图预览\n**当前状态**: 图层卡片仅显示类型图标\n\n**目标**:\n- 在图层卡片中显示缩略图\n- 提升图层识别度\n\n**实施要点**:\n- 为 `ImageLayer` 生成缩略图（缓存）\n- 为 `ShapeLayer` 生成预览图\n- 使用 `remember` 缓存缩略图，避免重复计算\n\n**预计工作量**: 2-3 天\n\n---\n\n### 1.2 图像层交互增强\n\n#### 1.2.1 旋转和缩放交互 ⭐ 高优先级\n**当前状态**: 仅支持拖动位置\n\n**目标**:\n- 添加旋转手柄和控制点\n- 支持鼠标滚轮缩放\n- 支持右键旋转\n\n**实施要点**:\n```kotlin\n// 在图像层周围添加控制点\ndata class ImageLayerControls(\n    val translation: Offset,\n    val rotation: Float,\n    val scale: Float,\n    val pivot: Offset\n)\n\n// 添加交互处理\nfun handleImageLayerTransform(\n    layerId: UUID,\n    transformType: TransformType,\n    value: Float\n)\n```\n\n**技术细节**:\n- 在 Canvas 上绘制控制点和旋转手柄\n- 检测鼠标悬停和拖动\n- 更新 `LayerTransform` 的 `rotation` 和 `scaleX/Y`\n\n**预计工作量**: 5-7 天\n\n---\n\n#### 1.2.2 图像层裁剪\n**当前状态**: 不支持裁剪\n\n**目标**:\n- 支持裁剪区域选择\n- 添加遮罩功能\n\n**实施要点**:\n- 在 `ImageLayer` 中添加 `cropRect` 属性\n- 渲染时应用裁剪区域\n- UI 上显示裁剪控制点\n\n**预计工作量**: 7-10 天\n\n---\n\n### 1.3 形状层功能扩展\n\n#### 1.3.1 解除形状层数量限制（可选）\n**当前状态**: 限制最多 1 个形状层（`MAX_SHAPE_LAYERS = 1`）\n\n**目标**:\n- 评估是否需要支持多个形状层\n- 如需要，重构相关逻辑\n\n**考虑因素**:\n- 用户需求是否强烈\n- 实现复杂度\n- 对现有代码的影响\n\n**预计工作量**: 5-10 天（取决于重构范围）\n\n---\n\n#### 1.3.2 形状层分组\n**当前状态**: 不支持分组\n\n**目标**:\n- 支持形状分组管理\n- 分组级别的可见性/锁定控制\n\n**预计工作量**: 10-15 天\n\n---\n\n## 二、性能优化\n\n### 2.1 渲染性能优化\n\n#### 2.1.1 图层渲染缓存 ⭐ 中优先级\n**当前状态**: 每次重绘都重新渲染所有图层\n\n**目标**:\n- 对未变化的图层使用缓存\n- 减少不必要的重绘\n\n**实施要点**:\n```kotlin\nclass LayerRenderer {\n    private val renderCache = mutableMapOf<UUID, ImageBitmap>()\n    \n    fun drawAll(drawScope: DrawScope, layers: List<Layer>) {\n        layers.forEach { layer ->\n            if (layer.isDirty) {\n                renderCache.remove(layer.id)\n                layer.isDirty = false\n            }\n            \n            val cached = renderCache[layer.id]\n            if (cached != null && !layer.isDirty) {\n                // 使用缓存\n                drawScope.drawImage(cached)\n            } else {\n                // 重新渲染并缓存\n                val rendered = renderLayer(layer, drawScope)\n                renderCache[layer.id] = rendered\n            }\n        }\n    }\n}\n```\n\n**技术细节**:\n- 在 `Layer` 基类中添加 `isDirty` 标记\n- 图层属性变化时设置 `isDirty = true`\n- 使用 `remember` 在 Compose 中缓存渲染结果\n\n**预计工作量**: 5-7 天\n\n---\n\n#### 2.1.2 增量渲染\n**当前状态**: 所有图层每次都重绘\n\n**目标**:\n- 只重绘变化的图层\n- 优化 Compose 重组\n\n**实施要点**:\n- 使用 `LaunchedEffect` 监听图层变化\n- 只更新变化的图层区域\n- 使用 `Modifier.drawWithCache` 优化绘制\n\n**预计工作量**: 7-10 天\n\n---\n\n#### 2.1.3 大图像优化\n**当前状态**: 可能对超大图像性能不佳\n\n**目标**:\n- 对超大图像使用缩略图预览\n- 导出时使用全分辨率\n\n**实施要点**:\n- 在 `ImageLayer` 中维护缩略图\n- 渲染时使用缩略图，导出时使用原图\n- 实现渐进式加载\n\n**预计工作量**: 5-7 天\n\n---\n\n### 2.2 内存优化\n\n#### 2.2.1 图像层内存管理\n**目标**:\n- 实现图像压缩/解压缩策略\n- 对不可见图层延迟加载\n\n**实施要点**:\n- 使用 `SoftReference` 缓存图像\n- 实现 LRU 缓存策略\n- 对不可见图层不加载到内存\n\n**预计工作量**: 7-10 天\n\n---\n\n#### 2.2.2 形状数据优化\n**目标**:\n- 使用更高效的数据结构\n- 考虑使用 `Path` 对象缓存\n\n**实施要点**:\n- 评估当前 `SnapshotStateMap` 的性能\n- 考虑使用 `Path` 对象缓存复杂形状\n- 实现形状数据的序列化/反序列化\n\n**预计工作量**: 3-5 天\n\n---\n\n## 三、代码质量提升\n\n### 3.1 测试覆盖\n\n#### 3.1.1 单元测试扩展\n**当前状态**: 已有基础测试（`LayerManagerTest.kt`、`ExportManagerTest.kt`）\n\n**目标**:\n- 提高测试覆盖率到 80% 以上\n- 覆盖边界情况和异常情况\n\n**需要测试的场景**:\n- `ImageLayer.render()` 的边界情况（零尺寸、空图像等）\n- `LayerManager` 的并发安全测试\n- 坐标转换器的各种场景\n- 图层变换的数学计算\n\n**预计工作量**: 10-15 天\n\n---\n\n#### 3.1.2 集成测试\n**目标**:\n- 图层合成导出测试\n- UI 交互测试\n\n**实施要点**:\n- 使用 Compose 测试框架\n- 测试图层操作的完整流程\n- 测试导出结果的正确性\n\n**预计工作量**: 7-10 天\n\n---\n\n### 3.2 错误处理\n\n#### 3.2.1 异常处理完善\n**目标**:\n- 图像加载失败处理\n- 渲染错误恢复机制\n\n**实施要点**:\n```kotlin\n// 在 ImageLayer 中添加错误处理\nfun updateImage(newImage: ImageBitmap?) {\n    try {\n        image = newImage\n    } catch (e: Exception) {\n        logger.error(\"更新图像失败\", e)\n        // 显示错误提示\n        // 恢复上一个有效图像\n    }\n}\n```\n\n**预计工作量**: 3-5 天\n\n---\n\n#### 3.2.2 用户反馈改进\n**目标**:\n- 更明确的错误提示\n- 操作成功/失败的 Toast 提示\n\n**实施要点**:\n- 统一错误消息格式\n- 添加操作成功提示\n- 提供错误恢复建议\n\n**预计工作量**: 2-3 天\n\n---\n\n### 3.3 代码重构\n\n#### 3.3.1 背景层管理抽象\n**当前状态**: 背景层管理逻辑分散在 `ShapeDrawingView` 中\n\n**目标**:\n- 创建 `BackgroundLayerManager` 统一管理背景层\n\n**实施要点**:\n```kotlin\nclass BackgroundLayerManager(\n    private val layerManager: LayerManager\n) {\n    private val BACKGROUND_LAYER_NAME = \"背景图层\"\n    \n    fun getOrCreateBackgroundLayer(image: ImageBitmap): ImageLayer {\n        val existing = layerManager.layers.value\n            .firstOrNull { it.name == BACKGROUND_LAYER_NAME && it is ImageLayer } \n            as? ImageLayer\n        \n        return existing ?: run {\n            val newLayer = ImageLayer(BACKGROUND_LAYER_NAME, image)\n            layerManager.addLayer(newLayer, index = 0)\n            newLayer\n        }\n    }\n    \n    fun updateBackgroundLayer(image: ImageBitmap) {\n        val layer = getOrCreateBackgroundLayer(image)\n        layer.updateImage(image)\n    }\n}\n```\n\n**预计工作量**: 2-3 天\n\n---\n\n#### 3.3.2 图层操作命令模式\n**目标**:\n- 实现撤销/重做功能\n- 使用命令模式封装图层操作\n\n**实施要点**:\n```kotlin\ninterface LayerCommand {\n    fun execute()\n    fun undo()\n}\n\nclass AddLayerCommand(\n    private val layerManager: LayerManager,\n    private val layer: Layer\n) : LayerCommand {\n    override fun execute() {\n        layerManager.addLayer(layer)\n    }\n    \n    override fun undo() {\n        layerManager.removeLayer(layer.id)\n    }\n}\n\nclass CommandManager {\n    private val undoStack = mutableListOf<LayerCommand>()\n    private val redoStack = mutableListOf<LayerCommand>()\n    \n    fun execute(command: LayerCommand) {\n        command.execute()\n        undoStack.add(command)\n        redoStack.clear()\n    }\n    \n    fun undo() {\n        if (undoStack.isNotEmpty()) {\n            val command = undoStack.removeLast()\n            command.undo()\n            redoStack.add(command)\n        }\n    }\n    \n    fun redo() {\n        if (redoStack.isNotEmpty()) {\n            val command = redoStack.removeLast()\n            command.execute()\n            undoStack.add(command)\n        }\n    }\n}\n```\n\n**预计工作量**: 10-15 天\n\n---\n\n## 四、架构优化\n\n### 4.1 图层类型扩展\n\n#### 4.1.1 文本层（独立图层类型）\n**当前状态**: 文本在形状层中\n\n**目标**:\n- 创建独立的 `TextLayer` 类型\n- 提供更专业的文本编辑功能\n\n**实施要点**:\n```kotlin\nclass TextLayer(\n    name: String,\n    var text: String = \"\",\n    var font: Font = Font.Default,\n    var fontSize: Float = 16f,\n    var color: Color = Color.Black,\n    var position: Offset = Offset.Zero\n) : Layer(\n    type = LayerType.TEXT,\n    name = name\n) {\n    override fun render(drawScope: DrawScope) {\n        // 文本渲染逻辑\n    }\n}\n```\n\n**预计工作量**: 7-10 天\n\n---\n\n#### 4.1.2 调整层（Adjustment Layer）\n**目标**:\n- 亮度、对比度、色彩调整\n- 不影响原始图像数据\n\n**实施要点**:\n```kotlin\nclass AdjustmentLayer(\n    name: String,\n    var brightness: Float = 0f,\n    var contrast: Float = 1f,\n    var saturation: Float = 1f\n) : Layer(\n    type = LayerType.ADJUSTMENT,\n    name = name\n) {\n    override fun render(drawScope: DrawScope) {\n        // 应用调整效果到下层图层\n    }\n}\n```\n\n**预计工作量**: 15-20 天\n\n---\n\n#### 4.1.3 滤镜层\n**目标**:\n- 模糊、锐化等效果\n- 可叠加多个滤镜\n\n**实施要点**:\n```kotlin\nenum class FilterType {\n    BLUR,\n    SHARPEN,\n    EMBOSS,\n    // ...\n}\n\nclass FilterLayer(\n    name: String,\n    var filterType: FilterType,\n    var intensity: Float = 1f\n) : Layer(\n    type = LayerType.FILTER,\n    name = name\n)\n```\n\n**预计工作量**: 20-30 天\n\n---\n\n### 4.2 混合模式支持\n\n#### 4.2.1 实现混合模式\n**目标**:\n- 支持多种混合模式（Normal、Multiply、Screen 等）\n- 在图层合成时应用混合模式\n\n**实施要点**:\n```kotlin\nenum class BlendMode {\n    NORMAL,\n    MULTIPLY,\n    SCREEN,\n    OVERLAY,\n    SOFT_LIGHT,\n    HARD_LIGHT,\n    COLOR_DODGE,\n    COLOR_BURN,\n    DARKEN,\n    LIGHTEN,\n    DIFFERENCE,\n    EXCLUSION\n}\n\nclass Layer {\n    var blendMode: BlendMode = BlendMode.NORMAL\n}\n\n// 在 LayerRenderer 中应用混合模式\nfun drawAll(drawScope: DrawScope, layers: List<Layer>) {\n    layers.forEach { layer ->\n        drawScope.drawIntoCanvas { canvas ->\n            // 应用混合模式\n            val paint = Paint().apply {\n                blendMode = when (layer.blendMode) {\n                    BlendMode.NORMAL -> BlendMode.SrcOver\n                    BlendMode.MULTIPLY -> BlendMode.Multiply\n                    // ...\n                }\n            }\n            // 绘制图层\n        }\n    }\n}\n```\n\n**预计工作量**: 15-20 天\n\n---\n\n### 4.3 图层组（Layer Group）\n\n#### 4.3.1 实现图层分组\n**目标**:\n- 支持图层分组\n- 组级别的可见性/锁定控制\n- 嵌套分组支持\n\n**实施要点**:\n```kotlin\nclass LayerGroup(\n    name: String,\n    val children: MutableList<Layer> = mutableListOf()\n) : Layer(\n    type = LayerType.GROUP,\n    name = name\n) {\n    fun addChild(layer: Layer) {\n        children.add(layer)\n    }\n    \n    fun removeChild(layerId: UUID) {\n        children.removeAll { it.id == layerId }\n    }\n    \n    override fun render(drawScope: DrawScope) {\n        if (!visible) return\n        children.forEach { child ->\n            if (child.visible) {\n                child.render(drawScope)\n            }\n        }\n    }\n}\n```\n\n**预计工作量**: 20-30 天\n\n---\n\n## 五、用户体验改进\n\n### 5.1 快捷键支持 ⭐ 高优先级\n\n#### 5.1.1 实现常用快捷键\n**目标**:\n- 提供键盘快捷键支持\n- 提升操作效率\n\n**快捷键列表**:\n- `Ctrl/Cmd + D` - 复制图层\n- `Delete` / `Backspace` - 删除图层\n- `Ctrl/Cmd + G` - 创建图层组\n- `Ctrl/Cmd + Shift + N` - 新建图层\n- `Ctrl/Cmd + J` - 复制并新建图层\n- `Ctrl/Cmd + Shift + ]` - 图层上移\n- `Ctrl/Cmd + Shift + [` - 图层下移\n- `Ctrl/Cmd + Z` - 撤销（如果实现）\n- `Ctrl/Cmd + Shift + Z` - 重做（如果实现）\n\n**实施要点**:\n```kotlin\n// 在 ShapeDrawingView 中添加键盘事件处理\nModifier.onKeyEvent { keyEvent ->\n    when {\n        keyEvent.isCtrlPressed && keyEvent.key == Key.D -> {\n            // 复制图层\n            true\n        }\n        keyEvent.key == Key.Delete -> {\n            // 删除图层\n            true\n        }\n        else -> false\n    }\n}\n```\n\n**预计工作量**: 5-7 天\n\n---\n\n### 5.2 图层搜索/过滤\n\n#### 5.2.1 实现搜索功能\n**目标**:\n- 按名称搜索图层\n- 按类型过滤\n- 显示/隐藏空图层\n\n**实施要点**:\n```kotlin\n@Composable\nfun LayerPanel(\n    editorController: EditorController,\n    state: ApplicationState,\n    modifier: Modifier = Modifier\n) {\n    var searchQuery by remember { mutableStateOf(\"\") }\n    var filterType by remember { mutableStateOf<LayerType?>(null) }\n    \n    val filteredLayers = remember(layers, searchQuery, filterType) {\n        layers.filter { layer ->\n            (searchQuery.isEmpty() || layer.name.contains(searchQuery, ignoreCase = true)) &&\n            (filterType == null || layer.type == filterType)\n        }\n    }\n    \n    // UI 实现\n}\n```\n\n**预计工作量**: 3-5 天\n\n---\n\n### 5.3 批量操作\n\n#### 5.3.1 实现多选功能\n**目标**:\n- 支持多选图层\n- 批量锁定/解锁\n- 批量重命名\n\n**实施要点**:\n```kotlin\nclass LayerManager {\n    private val _selectedLayers = MutableStateFlow<Set<UUID>>(emptySet())\n    val selectedLayers: StateFlow<Set<UUID>> = _selectedLayers.asStateFlow()\n    \n    fun selectLayer(layerId: UUID, multiSelect: Boolean = false) {\n        if (multiSelect) {\n            _selectedLayers.value = _selectedLayers.value.toMutableSet().apply {\n                if (contains(layerId)) remove(layerId) else add(layerId)\n            }\n        } else {\n            _selectedLayers.value = setOf(layerId)\n        }\n    }\n    \n    fun batchLock(locked: Boolean) {\n        _selectedLayers.value.forEach { id ->\n            setLayerLocked(id, locked)\n        }\n    }\n}\n```\n\n**预计工作量**: 7-10 天\n\n---\n\n## 六、实施计划\n\n### 6.1 短期计划（1-2 周）\n\n**优先级**: ⭐⭐⭐ 最高\n\n1. **图层删除功能**（2-3 天）\n   - 添加删除按钮\n   - 实现背景层保护\n   - 添加确认对话框\n\n2. **拖拽排序**（3-5 天）\n   - 实现拖拽交互\n   - 更新图层顺序\n\n3. **错误处理完善**（2-3 天）\n   - 完善异常处理\n   - 改进用户提示\n\n**总工作量**: 7-11 天\n\n---\n\n### 6.2 中期计划（1-2 月）\n\n**优先级**: ⭐⭐ 高\n\n1. **图像层旋转/缩放交互**（5-7 天）\n   - 添加控制点\n   - 实现交互逻辑\n\n2. **渲染性能优化**（5-7 天）\n   - 实现渲染缓存\n   - 优化重绘逻辑\n\n3. **撤销/重做功能**（10-15 天）\n   - 实现命令模式\n   - 添加撤销/重做 UI\n\n4. **快捷键支持**（5-7 天）\n   - 实现常用快捷键\n   - 添加快捷键提示\n\n**总工作量**: 25-36 天\n\n---\n\n### 6.3 长期计划（3-6 月）\n\n**优先级**: ⭐ 中\n\n1. **混合模式支持**（15-20 天）\n   - 实现各种混合模式\n   - 添加 UI 选择器\n\n2. **图层组功能**（20-30 天）\n   - 实现分组逻辑\n   - 添加分组 UI\n\n3. **调整层和滤镜层**（35-50 天）\n   - 实现调整层\n   - 实现滤镜层\n   - 添加效果预览\n\n4. **测试覆盖扩展**（10-15 天）\n   - 扩展单元测试\n   - 添加集成测试\n\n**总工作量**: 80-115 天\n\n---\n\n## 七、技术债务清理\n\n### 7.1 代码清理\n\n#### 7.1.1 移除未使用的代码\n- 检查是否有废弃的 API\n- 清理注释掉的代码\n- 移除未使用的导入\n\n**预计工作量**: 1-2 天\n\n---\n\n#### 7.1.2 文档完善\n- 添加 API 文档（KDoc）\n- 创建使用示例\n- 编写架构决策记录（ADR）\n\n**预计工作量**: 3-5 天\n\n---\n\n#### 7.1.3 代码规范\n- 统一命名规范\n- 代码格式化\n- 添加必要的注释\n\n**预计工作量**: 2-3 天\n\n---\n\n### 7.2 依赖管理\n\n#### 7.2.1 依赖更新\n- 定期更新依赖版本\n- 评估新版本的功能和性能改进\n- 处理废弃的 API\n\n**预计工作量**: 持续进行\n\n---\n\n## 八、监控与评估\n\n### 8.1 性能监控\n\n#### 8.1.1 关键指标\n- **渲染帧率**: 目标 60 FPS\n- **内存使用**: 监控峰值内存\n- **导出耗时**: 目标 < 2 秒（普通图像）\n\n**实施要点**:\n```kotlin\n// 添加性能监控\nclass PerformanceMonitor {\n    fun measureRenderTime(block: () -> Unit): Long {\n        val start = System.currentTimeMillis()\n        block()\n        return System.currentTimeMillis() - start\n    }\n    \n    fun logMemoryUsage() {\n        val runtime = Runtime.getRuntime()\n        val used = runtime.totalMemory() - runtime.freeMemory()\n        logger.info(\"内存使用: ${used / 1024 / 1024} MB\")\n    }\n}\n```\n\n---\n\n### 8.2 用户反馈\n\n#### 8.2.1 反馈收集\n- 收集使用痛点\n- 功能需求优先级\n- Bug 报告\n\n**实施要点**:\n- 在应用中添加反馈入口\n- 定期收集用户意见\n- 建立需求优先级评估机制\n\n---\n\n### 8.3 代码质量指标\n\n#### 8.3.1 质量指标\n- **测试覆盖率**: 目标 80% 以上\n- **代码复杂度**: 使用工具分析（如 SonarQube）\n- **技术债务**: 定期评估和清理\n\n**实施要点**:\n- 使用代码质量工具\n- 定期代码审查\n- 技术债务跟踪\n\n---\n\n## 九、风险评估\n\n### 9.1 技术风险\n\n1. **性能风险**\n   - 大量图层可能导致性能下降\n   - **缓解措施**: 实现渲染缓存和增量渲染\n\n2. **兼容性风险**\n   - 新功能可能影响现有功能\n   - **缓解措施**: 充分测试，渐进式发布\n\n3. **复杂度风险**\n   - 功能增加可能导致代码复杂度上升\n   - **缓解措施**: 代码重构，模块化设计\n\n---\n\n### 9.2 时间风险\n\n1. **估算不准确**\n   - 实际工作量可能超过估算\n   - **缓解措施**: 预留缓冲时间，分阶段实施\n\n2. **优先级冲突**\n   - 多个高优先级任务可能冲突\n   - **缓解措施**: 明确优先级，合理分配资源\n\n---\n\n## 十、总结\n\n本路线图提供了图层系统优化的全面规划，涵盖了功能扩展、性能优化、代码质量提升、架构优化、用户体验改进等多个方面。\n\n**建议实施顺序**:\n1. 先完成短期计划（1-2 周），快速提升用户体验\n2. 然后进行中期计划（1-2 月），优化性能和添加核心功能\n3. 最后推进长期计划（3-6 月），实现高级功能和架构优化\n\n**关键成功因素**:\n- 持续的用户反馈收集\n- 定期的性能监控和优化\n- 代码质量保证（测试、文档、规范）\n- 渐进式实施，避免大范围重构\n\n---\n\n**文档维护**: 本文档应随着项目进展定期更新，记录实际完成情况、遇到的问题和调整的计划。\n\n"
  },
  {
    "path": "domain/build.gradle.kts",
    "content": "plugins {\n    kotlin(\"jvm\")\n}\n\nrepositories {\n    mavenCentral()\n}\n\ndependencies {\n    testImplementation(kotlin(\"test\"))\n    implementation (\"org.jetbrains.kotlin:kotlin-stdlib\")\n}\n\ntasks.test {\n    useJUnitPlatform()\n}\nkotlin {\n    jvmToolchain(17)\n}"
  },
  {
    "path": "domain/src/main/kotlin/cn/netdiscovery/monica/domain/ColorCorrectionSettings.kt",
    "content": "package cn.netdiscovery.monica.domain\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.domain.ColorCorrectionSettings\n * @author: Tony Shen\n * @date: 2024/11/6 10:40\n * @version: V1.0 <描述当前版本功能>\n */\ndata class ColorCorrectionSettings(\n    val contrast:Int = 255,     // 对比度，范围 0-510\n    val hue:Int = 180,          // 色调，范围 0-360\n    val saturation:Int = 255,   // 饱和度，范围 0-510\n    val lightness:Int = 255,    // 亮度，范围 0-510\n    val temperature:Int = 255,  // 色温，范围 0-510\n    val highlight:Int = 255,    // 高光，范围 0-510\n    val shadow:Int = 255,       // 阴影，范围 0-510\n    val sharpen:Int = 0,        // 锐化，范围 0-255\n    val corner:Int = 0,         // 暗角，范围 0-255\n\n    val status:Int = 0 // 1 contrast, 2 hue, 3 saturation, 4 lightness, 5 temperature, 6 highlight, 7 shadow, 8 sharpen, 9 corner\n)"
  },
  {
    "path": "domain/src/main/kotlin/cn/netdiscovery/monica/domain/ContourDisplaySettings.kt",
    "content": "package cn.netdiscovery.monica.domain\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.ai.experiment.model.ContourDisplaySettings\n * @author: Tony Shen\n * @date: 2024/10/29 14:26\n * @version: V1.0 <描述当前版本功能>\n */\ndata class ContourDisplaySettings(\n    var showOriginalImage: Boolean = false,\n    var showBoundingRect: Boolean = false,\n    var showMinAreaRect: Boolean = false,\n    var showCenter: Boolean = false\n)"
  },
  {
    "path": "domain/src/main/kotlin/cn/netdiscovery/monica/domain/ContourFilterSettings.kt",
    "content": "package cn.netdiscovery.monica.domain\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.ai.experiment.model.ContourFilterSettings\n * @author: Tony Shen\n * @date: 2024/10/29 17:52\n * @version: V1.0 <描述当前版本功能>\n */\ndata class ContourFilterSettings (\n    var minPerimeter:Double = 0.0,\n    var maxPerimeter:Double = 0.0,\n\n    var minArea:Double = 0.0,\n    var maxArea:Double = 0.0,\n\n    var minRoundness:Double = 0.0,\n    var maxRoundness:Double = 0.0,\n\n    var minAspectRatio:Double = 0.0,\n    var maxAspectRatio:Double = 0.0\n)"
  },
  {
    "path": "domain/src/main/kotlin/cn/netdiscovery/monica/domain/DecodedPreviewImage.kt",
    "content": "package cn.netdiscovery.monica.domain\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.domain.DecodedPreviewImage\n * @author: Tony Shen\n * @date: 2025/7/21 12:40\n * @version: V1.0 <描述当前版本功能>\n */\ndata class DecodedPreviewImage(\n    val nativePtr: Long,  // 对应 MonicaImageProcess 中 PyramidImage 对象的指针地址\n    val width: Int,\n    val height: Int,\n    val previewImage: IntArray // 返回金字塔第一层的图像\n) {\n    override fun equals(other: Any?): Boolean {\n        if (this === other) return true\n        if (javaClass != other?.javaClass) return false\n\n        other as DecodedPreviewImage\n\n        if (nativePtr != other.nativePtr) return false\n        if (width != other.width) return false\n        if (height != other.height) return false\n        if (!previewImage.contentEquals(other.previewImage)) return false\n\n        return true\n    }\n\n    override fun hashCode(): Int {\n        var result = nativePtr.hashCode()\n        result = 31 * result + width\n        result = 31 * result + height\n        result = 31 * result + previewImage.contentHashCode()\n        return result\n    }\n}\n"
  },
  {
    "path": "domain/src/main/kotlin/cn/netdiscovery/monica/domain/GeneralSettings.kt",
    "content": "package cn.netdiscovery.monica.domain\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.domain.GeneralSettings\n * @author: Tony Shen\n * @date: 2025/2/7 10:27\n * @version: V1.0 <描述当前版本功能>\n */\ndata class GeneralSettings(\n    var outputBoxR: Int,\n    var outputBoxG: Int,\n    var outputBoxB: Int,\n    var size: Int,\n    var maxHistorySize: Int,\n    var deepSeekApiKey: String,\n    var geminiApiKey: String,\n    var algorithmUrl: String,\n    var themeId: String = \"LIGHT\"\n)"
  },
  {
    "path": "domain/src/main/kotlin/cn/netdiscovery/monica/domain/MatchTemplateSettings.kt",
    "content": "package cn.netdiscovery.monica.domain\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.ai.experiment.model.MatchTemplateSettings\n * @author: Tony Shen\n * @date: 2025/1/4 20:29\n * @version: V1.0 <描述当前版本功能>\n */\ndata class MatchTemplateSettings (\n    var matchType:Int = 0,                   // 0 表示原图匹配，1 表示灰度匹配 2 表示边缘匹配\n    var angleStart:Int = 0,\n    var angleEnd:Int = 360,\n    var angleStep:Int = 10,\n    var scaleStart:Double = 0.0,\n    var scaleEnd:Double = 1.0,\n    var scaleStep:Double = 0.1,\n    var matchTemplateThreshold:Double = 0.8, // 模版匹配的阈值\n    var scoreThreshold: Float = 0.6f ,       // 置信分数的阈值(nms 相关)\n    var nmsThreshold: Float = 0.3f           // 非极大值抑制的阈值(nms 相关)\n)"
  },
  {
    "path": "domain/src/main/kotlin/cn/netdiscovery/monica/domain/MorphologicalOperationSettings.kt",
    "content": "package cn.netdiscovery.monica.domain\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.ai.experiment.model.MorphologicalOperationSettings\n * @author: Tony Shen\n * @date: 2024/12/26 20:42\n * @version: V1.0 <描述当前版本功能>\n */\ndata class MorphologicalOperationSettings(\n    var op:Int = 0,\n    var shape:Int = 0,\n    var width:Int = 0,\n    var height:Int = 0\n)"
  },
  {
    "path": "domain/src/main/kotlin/cn/netdiscovery/monica/domain/NativeImage.kt",
    "content": "package cn.netdiscovery.monica.domain\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.domain.NativeImage\n * @author: Tony Shen\n * @date: 2025/7/22 14:20\n * @version: V1.0 <描述当前版本功能>\n */\ndata class NativeImage(\n    val width: Int,\n    val height: Int,\n    val pixels: IntArray\n) {\n    override fun equals(other: Any?): Boolean {\n        if (this === other) return true\n        if (javaClass != other?.javaClass) return false\n\n        other as NativeImage\n\n        if (width != other.width) return false\n        if (height != other.height) return false\n        if (!pixels.contentEquals(other.pixels)) return false\n\n        return true\n    }\n\n    override fun hashCode(): Int {\n        var result = width\n        result = 31 * result + height\n        result = 31 * result + pixels.contentHashCode()\n        return result\n    }\n}\n"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "content": "distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-8.7-bin.zip\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists"
  },
  {
    "path": "gradle.properties",
    "content": "kotlin.code.style=official\napp.version=1.1.5\nkotlin.version=2.1.0\nagp.version=7.3.0\ncompose.version=1.6.11\nkotlinx.coroutines.core.version=1.8.1-Beta\nkoin.compose=4.0.0\n\nlogback=1.2.3\ncolormath=3.5.0\ntwelvemonkeys=3.12.0\nbatik=1.19\n\nrxcache=2.2.0\ncoroutines.utils=v1.1.8"
  },
  {
    "path": "gradlew",
    "content": "#!/bin/sh\n\n#\n# Copyright © 2015-2021 the original authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      https://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n##############################################################################\n#\n#   Gradle start up script for POSIX generated by Gradle.\n#\n#   Important for running:\n#\n#   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is\n#       noncompliant, but you have some other compliant shell such as ksh or\n#       bash, then to run this script, type that shell name before the whole\n#       command line, like:\n#\n#           ksh Gradle\n#\n#       Busybox and similar reduced shells will NOT work, because this script\n#       requires all of these POSIX shell features:\n#         * functions;\n#         * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,\n#           «${var#prefix}», «${var%suffix}», and «$( cmd )»;\n#         * compound commands having a testable exit status, especially «case»;\n#         * various built-in commands including «command», «set», and «ulimit».\n#\n#   Important for patching:\n#\n#   (2) This script targets any POSIX shell, so it avoids extensions provided\n#       by Bash, Ksh, etc; in particular arrays are avoided.\n#\n#       The \"traditional\" practice of packing multiple parameters into a\n#       space-separated string is a well documented source of bugs and security\n#       problems, so this is (mostly) avoided, by progressively accumulating\n#       options in \"$@\", and eventually passing that to Java.\n#\n#       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,\n#       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;\n#       see the in-line comments for details.\n#\n#       There are tweaks for specific operating systems such as AIX, CygWin,\n#       Darwin, MinGW, and NonStop.\n#\n#   (3) This script is generated from the Groovy template\n#       https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt\n#       within the Gradle project.\n#\n#       You can find Gradle at https://github.com/gradle/gradle/.\n#\n##############################################################################\n\n# Attempt to set APP_HOME\n\n# Resolve links: $0 may be a link\napp_path=$0\n\n# Need this for daisy-chained symlinks.\nwhile\n    APP_HOME=${app_path%\"${app_path##*/}\"}  # leaves a trailing /; empty if no leading path\n    [ -h \"$app_path\" ]\ndo\n    ls=$( ls -ld \"$app_path\" )\n    link=${ls#*' -> '}\n    case $link in             #(\n      /*)   app_path=$link ;; #(\n      *)    app_path=$APP_HOME$link ;;\n    esac\ndone\n\nAPP_HOME=$( cd \"${APP_HOME:-./}\" && pwd -P ) || exit\n\nAPP_NAME=\"Gradle\"\nAPP_BASE_NAME=${0##*/}\n\n# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nDEFAULT_JVM_OPTS='\"-Xmx64m\" \"-Xms64m\"'\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=maximum\n\nwarn () {\n    echo \"$*\"\n} >&2\n\ndie () {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n} >&2\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\nnonstop=false\ncase \"$( uname )\" in                #(\n  CYGWIN* )         cygwin=true  ;; #(\n  Darwin* )         darwin=true  ;; #(\n  MSYS* | MINGW* )  msys=true    ;; #(\n  NONSTOP* )        nonstop=true ;;\nesac\n\nCLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar\n\n\n# Determine the Java command to use to start the JVM.\nif [ -n \"$JAVA_HOME\" ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n        # IBM's JDK on AIX uses strange locations for the executables\n        JAVACMD=$JAVA_HOME/jre/sh/java\n    else\n        JAVACMD=$JAVA_HOME/bin/java\n    fi\n    if [ ! -x \"$JAVACMD\" ] ; then\n        die \"ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nelse\n    JAVACMD=java\n    which java >/dev/null 2>&1 || die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\nfi\n\n# Increase the maximum file descriptors if we can.\nif ! \"$cygwin\" && ! \"$darwin\" && ! \"$nonstop\" ; then\n    case $MAX_FD in #(\n      max*)\n        MAX_FD=$( ulimit -H -n ) ||\n            warn \"Could not query maximum file descriptor limit\"\n    esac\n    case $MAX_FD in  #(\n      '' | soft) :;; #(\n      *)\n        ulimit -n \"$MAX_FD\" ||\n            warn \"Could not set maximum file descriptor limit to $MAX_FD\"\n    esac\nfi\n\n# Collect all arguments for the java command, stacking in reverse order:\n#   * args from the command line\n#   * the main class name\n#   * -classpath\n#   * -D...appname settings\n#   * --module-path (only if needed)\n#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.\n\n# For Cygwin or MSYS, switch paths to Windows format before running java\nif \"$cygwin\" || \"$msys\" ; then\n    APP_HOME=$( cygpath --path --mixed \"$APP_HOME\" )\n    CLASSPATH=$( cygpath --path --mixed \"$CLASSPATH\" )\n\n    JAVACMD=$( cygpath --unix \"$JAVACMD\" )\n\n    # Now convert the arguments - kludge to limit ourselves to /bin/sh\n    for arg do\n        if\n            case $arg in                                #(\n              -*)   false ;;                            # don't mess with options #(\n              /?*)  t=${arg#/} t=/${t%%/*}              # looks like a POSIX filepath\n                    [ -e \"$t\" ] ;;                      #(\n              *)    false ;;\n            esac\n        then\n            arg=$( cygpath --path --ignore --mixed \"$arg\" )\n        fi\n        # Roll the args list around exactly as many times as the number of\n        # args, so each arg winds up back in the position where it started, but\n        # possibly modified.\n        #\n        # NB: a `for` loop captures its iteration list before it begins, so\n        # changing the positional parameters here affects neither the number of\n        # iterations, nor the values presented in `arg`.\n        shift                   # remove old arg\n        set -- \"$@\" \"$arg\"      # push replacement arg\n    done\nfi\n\n# Collect all arguments for the java command;\n#   * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of\n#     shell script including quotes and variable substitutions, so put them in\n#     double quotes to make sure that they get re-expanded; and\n#   * put everything else in single quotes, so that it's not re-expanded.\n\nset -- \\\n        \"-Dorg.gradle.appname=$APP_BASE_NAME\" \\\n        -classpath \"$CLASSPATH\" \\\n        org.gradle.wrapper.GradleWrapperMain \\\n        \"$@\"\n\n# Use \"xargs\" to parse quoted args.\n#\n# With -n1 it outputs one arg per line, with the quotes and backslashes removed.\n#\n# In Bash we could simply go:\n#\n#   readarray ARGS < <( xargs -n1 <<<\"$var\" ) &&\n#   set -- \"${ARGS[@]}\" \"$@\"\n#\n# but POSIX shell has neither arrays nor command substitution, so instead we\n# post-process each arg (as a line of input to sed) to backslash-escape any\n# character that might be a shell metacharacter, then use eval to reverse\n# that process (while maintaining the separation between arguments), and wrap\n# the whole thing up as a single \"set\" statement.\n#\n# This will of course break if any of these variables contains a newline or\n# an unmatched quote.\n#\n\neval \"set -- $(\n        printf '%s\\n' \"$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS\" |\n        xargs -n1 |\n        sed ' s~[^-[:alnum:]+,./:=@_]~\\\\&~g; ' |\n        tr '\\n' ' '\n    )\" '\"$@\"'\n\nexec \"$JAVACMD\" \"$@\"\n"
  },
  {
    "path": "gradlew.bat",
    "content": "@rem\n@rem Copyright 2015 the original author or authors.\n@rem\n@rem Licensed under the Apache License, Version 2.0 (the \"License\");\n@rem you may not use this file except in compliance with the License.\n@rem You may obtain a copy of the License at\n@rem\n@rem      https://www.apache.org/licenses/LICENSE-2.0\n@rem\n@rem Unless required by applicable law or agreed to in writing, software\n@rem distributed under the License is distributed on an \"AS IS\" BASIS,\n@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n@rem See the License for the specific language governing permissions and\n@rem limitations under the License.\n@rem\n\n@if \"%DEBUG%\" == \"\" @echo off\n@rem ##########################################################################\n@rem\n@rem  Gradle startup script for Windows\n@rem\n@rem ##########################################################################\n\n@rem Set local scope for the variables with windows NT shell\nif \"%OS%\"==\"Windows_NT\" setlocal\n\nset DIRNAME=%~dp0\nif \"%DIRNAME%\" == \"\" set DIRNAME=.\nset APP_BASE_NAME=%~n0\nset APP_HOME=%DIRNAME%\n\n@rem Resolve any \".\" and \"..\" in APP_HOME to make it shorter.\nfor %%i in (\"%APP_HOME%\") do set APP_HOME=%%~fi\n\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nset DEFAULT_JVM_OPTS=\"-Xmx64m\" \"-Xms64m\"\n\n@rem Find java.exe\nif defined JAVA_HOME goto findJavaFromJavaHome\n\nset JAVA_EXE=java.exe\n%JAVA_EXE% -version >NUL 2>&1\nif \"%ERRORLEVEL%\" == \"0\" goto execute\n\necho.\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\necho.\necho Please set the JAVA_HOME variable in your environment to match the\necho location of your Java installation.\n\ngoto fail\n\n:findJavaFromJavaHome\nset JAVA_HOME=%JAVA_HOME:\"=%\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\n\nif exist \"%JAVA_EXE%\" goto execute\n\necho.\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%\necho.\necho Please set the JAVA_HOME variable in your environment to match the\necho location of your Java installation.\n\ngoto fail\n\n:execute\n@rem Setup the command line\n\nset CLASSPATH=%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\n\n\n@rem Execute Gradle\n\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% \"-Dorg.gradle.appname=%APP_BASE_NAME%\" -classpath \"%CLASSPATH%\" org.gradle.wrapper.GradleWrapperMain %*\n\n:end\n@rem End local scope for the variables with windows NT shell\nif \"%ERRORLEVEL%\"==\"0\" goto mainEnd\n\n:fail\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\nrem the _cmd.exe /c_ return code!\nif  not \"\" == \"%GRADLE_EXIT_CONSOLE%\" exit 1\nexit /b 1\n\n:mainEnd\nif \"%OS%\"==\"Windows_NT\" endlocal\n\n:omega\n"
  },
  {
    "path": "i18n/QUICK_REFERENCE.md",
    "content": "# i18n 脚本快速参考\n\n## 🚀 快速开始\n\n### 基本检查\n```bash\n# 检查中英文文件是否同步\n./string_manager.sh -m\n\n# 检查重复项（不修改文件）\n./string_manager.sh -c -d\n\n# 检查行号一致性\n./position_check.sh\n```\n\n### 修复重复项\n```bash\n# 自动修复重复项\n./string_manager.sh -c -f\n```\n\n## 📋 常用命令\n\n| 命令 | 功能 |\n|------|------|\n| `./string_manager.sh --help` | 显示帮助信息 |\n| `./string_manager.sh -m` | 比对中英文文件差异 |\n| `./string_manager.sh -c -d` | 检查重复项（不修改） |\n| `./string_manager.sh -c -f` | 自动修复重复项 |\n| `./string_manager.sh -c -v -d` | 详细检查重复项 |\n| `./string_manager.sh -a -f` | 全功能模式 |\n| `./position_check.sh` | 检查行号一致性 |\n\n## ⚡ 一键检查脚本\n\n创建一个检查脚本 `quick_check.sh`：\n\n```bash\n#!/bin/bash\necho \"=== i18n 文件快速检查 ===\"\necho \"\"\n\necho \"1. 检查中英文文件同步性...\"\n./string_manager.sh -m\necho \"\"\n\necho \"2. 检查重复项...\"\n./string_manager.sh -c -d\necho \"\"\n\necho \"3. 检查行号一致性...\"\n./position_check.sh\necho \"\"\n\necho \"=== 检查完成 ===\"\n```\n\n使用方法：\n```bash\nchmod +x quick_check.sh\n./quick_check.sh\n```\n\n## 🔧 故障排除\n\n### 权限问题\n```bash\nchmod +x string_manager.sh position_check.sh\n```\n\n### 文件不存在\n```bash\nls -la src/main/resources/strings/\n```\n\n### 语法检查\n```bash\nbash -n string_manager.sh\nbash -n position_check.sh\n```\n\n## 📊 输出解读\n\n### ✅ 正常状态\n- 中英文文件字符串数量相同\n- 无缺失翻译\n- 无重复字符串名称\n\n### ⚠️ 需要注意\n- 有重复字符串内容（正常，不同key可以有相同内容）\n- 行号不一致（正常，文件结构可能不同）\n\n### ❌ 需要修复\n- 有重复字符串名称\n- 有缺失的翻译\n- 文件不同步\n"
  },
  {
    "path": "i18n/README.md",
    "content": "# 🌍 Monica 国际化工具集\n\n## 📋 概述\n\nMonica 项目的国际化字符串资源管理工具集，提供完整的字符串资源文件管理解决方案。\n\n## 🛠️ 工具列表\n\n### 核心工具\n- **`string_manager.sh`** - 综合管理工具\n  - 清理重复项\n  - 检查缺失翻译\n  - 文件统计报告\n  - 自动修复功能\n\n- **`position_check.sh`** - 位置比对工具\n  - 检查中英文文件位置一致性\n  - 详细的位置差异分析\n  - 统计信息报告\n\n### 文档\n- **`SCRIPT_USAGE_GUIDE.md`** - 脚本使用指南\n  - 详细的使用说明和示例\n  - 所有选项和参数说明\n  - 输出解读和故障排除\n- **`QUICK_REFERENCE.md`** - 快速参考\n  - 常用命令速查\n  - 一键检查脚本\n  - 快速故障排除\n- **`I18N_STRING_MANAGEMENT_GUIDE.md`** - 完整使用指南\n  - 详细的使用说明\n  - 工作流程指导\n  - 最佳实践建议\n  - 故障排除指南\n\n## 🚀 快速开始\n\n```bash\n# 进入工具目录\ncd i18n\n\n# 一键检查所有问题\n./quick_check.sh\n\n# 或者单独使用各个工具\n./string_manager.sh -m    # 检查同步性\n./string_manager.sh -c -d # 检查重复项\n./position_check.sh       # 检查行号一致性\n```\n\n## 📖 详细文档\n\n- **`SCRIPT_USAGE_GUIDE.md`** - 脚本使用详细指南\n- **`QUICK_REFERENCE.md`** - 快速参考和常用命令\n- **`I18N_STRING_MANAGEMENT_GUIDE.md`** - 完整的国际化管理指南\n\n## 🎯 主要功能\n\n- ✅ 自动检测重复字符串\n- ✅ 比对中英文翻译完整性\n- ✅ 检查文件位置一致性\n- ✅ 生成详细统计报告\n- ✅ 自动备份和修复\n- ✅ 支持批量处理\n\n## 📊 当前状态\n\n- **字符串总数**: 366个\n- **文件状态**: 完整同步\n- **重复项**: 无重复字符串名称\n- **位置一致性**: 349个key位置不一致（正常现象）\n\n## 🔧 维护建议\n\n1. **日常**: 使用 `./quick_check.sh` 快速检查\n2. **开发新功能后**: 运行 `./string_manager.sh -m` 检查同步性\n3. **发现重复项**: 使用 `./string_manager.sh -c -f` 自动修复\n4. **定期维护**: 每月运行完整检查\n\n---\n\n**版本**: 2.0  \n**最后更新**: 2025-09-03  \n**维护者**: AI Assistant\n"
  },
  {
    "path": "i18n/SCRIPT_USAGE_GUIDE.md",
    "content": "# i18n 脚本使用指南\n\n## 📖 概述\n\n本指南介绍 i18n 模块中两个字符串资源管理脚本的使用方法：\n- `string_manager.sh` - 字符串资源文件综合管理工具\n- `position_check.sh` - 位置比对脚本\n\n## 🔧 string_manager.sh - 字符串资源文件综合管理工具\n\n### 功能特性\n\n- ✅ 检查重复的字符串名称和内容\n- ✅ 比对中英文文件差异\n- ✅ 自动修复重复项\n- ✅ 位置比对模式\n- ✅ 详细输出和干运行模式\n- ✅ 自动备份功能\n\n### 基本语法\n\n```bash\n./string_manager.sh [选项] <文件路径>\n```\n\n### 模式选项\n\n| 选项 | 长选项 | 功能 |\n|------|--------|------|\n| `-c` | `--cleanup` | 清理模式：检查并清理重复项 |\n| `-m` | `--compare` | 比对模式：比对中英文文件差异 |\n| `-p` | `--position` | 位置比对模式：检查相同key的行号是否一致 |\n| `-a` | `--all` | 全功能模式：清理 + 比对 |\n\n### 清理模式选项\n\n| 选项 | 长选项 | 功能 |\n|------|--------|------|\n| `-v` | `--verbose` | 详细输出 |\n| `-d` | `--dry-run` | 只检查，不修改文件 |\n| `-n` | `--no-backup` | 不创建备份文件 |\n| `-f` | `--auto-fix` | 自动修复重复项（保留第一次出现的版本） |\n\n### 使用示例\n\n#### 1. 查看帮助信息\n```bash\n./string_manager.sh --help\n```\n\n#### 2. 比对中英文文件差异\n```bash\n# 检查中英文文件是否同步\n./string_manager.sh -m\n```\n\n**输出示例：**\n```\n=== 中英文字符串资源文件比对 ===\n\n1. 提取中文文件中的字符串名称...\n2. 提取英文文件中的字符串名称...\n3. 统计信息...\n中文文件字符串数量:      366\n英文文件字符串数量:      366\n\n4. 中文有但英文没有的字符串:\n✅ 没有缺失的英文翻译\n\n5. 英文有但中文没有的字符串:\n✅ 没有缺失的中文翻译\n\n=== 比对完成 ===\n```\n\n#### 3. 检查重复项（不修改文件）\n```bash\n# 干运行模式，只检查不修改\n./string_manager.sh -c -d\n```\n\n**输出示例：**\n```\n字符串资源文件清理工具 v2.0\n\n处理文件: src/main/resources/strings/strings_zh.xml\n==================================\n=== 文件统计报告 ===\n文件: src/main/resources/strings/strings_zh.xml\n总行数:           470\n字符串总数: 366\n唯一字符串名称:           366\n重复字符串名称:        0\n\n✓ 没有发现重复的字符串名称\n发现 31 个重复的字符串内容:\n人脸替换\n人脸检测\n伽马变换\n...\n处理完成！\n```\n\n#### 4. 自动修复重复项\n```bash\n# 自动修复重复项，保留第一次出现的版本\n./string_manager.sh -c -f\n```\n\n#### 5. 详细检查模式\n```bash\n# 详细输出，显示重复项的详细信息\n./string_manager.sh -c -v -d\n```\n\n#### 6. 全功能模式\n```bash\n# 清理 + 比对，自动修复\n./string_manager.sh -a -f\n```\n\n#### 7. 检查特定文件\n```bash\n# 检查指定的文件\n./string_manager.sh -c src/main/resources/strings/strings_zh.xml\n```\n\n### 输出说明\n\n#### 文件统计报告\n- **总行数**: 文件的总行数\n- **字符串总数**: 包含的字符串定义数量\n- **唯一字符串名称**: 不重复的字符串名称数量\n- **重复字符串名称**: 重复的字符串名称数量\n\n#### 重复项检测\n- **重复字符串名称**: 相同 `name` 属性的字符串\n- **重复字符串内容**: 相同内容的字符串（可能名称不同）\n\n## 🔍 position_check.sh - 位置比对脚本\n\n### 功能特性\n\n- ✅ 检查中英文配置文件中相同key的行号是否一致\n- ✅ 统计行号差异信息\n- ✅ 提供详细的差异报告\n\n### 基本语法\n\n```bash\n./position_check.sh\n```\n\n### 使用示例\n\n#### 检查行号一致性\n```bash\n./position_check.sh\n```\n\n**输出示例：**\n```\n=== 中英文字符串资源文件位置比对 ===\n\n1. 提取中文文件中的key和行号...\n2. 提取英文文件中的key和行号...\n3. 统计信息...\n中文文件字符串数量: 366\n英文文件字符串数量: 366\n\n4. 共同key数量: 366\n\n5. 检查行号一致性...\n行号不匹配: adaptive_threshold_algorithm\n  中文文件第288行\n  英文文件第435行\n  差异: -147 行\n\n行号不匹配: adaptive_threshold_cancelled\n  中文文件第446行\n  英文文件第421行\n  差异: 25 行\n\n...\n\n6. 位置比对结果:\n⚠️  发现      349 个key的行号不一致\n  最大行号差异: 443 行\n  平均行号差异: 53.3 行\n\n=== 位置比对完成 ===\n```\n\n### 输出说明\n\n#### 统计信息\n- **中文文件字符串数量**: 中文文件中的字符串总数\n- **英文文件字符串数量**: 英文文件中的字符串总数\n- **共同key数量**: 两个文件都包含的字符串数量\n\n#### 行号差异\n- **行号不匹配**: 相同key在不同文件中的行号不同\n- **差异**: 行号差值（正数表示中文文件行号更大，负数表示英文文件行号更大）\n\n#### 比对结果\n- **不匹配数量**: 行号不一致的key数量\n- **最大行号差异**: 最大的行号差值\n- **平均行号差异**: 平均的行号差值\n\n## 🚀 常见使用场景\n\n### 场景1：日常维护检查\n```bash\n# 检查文件是否同步\n./string_manager.sh -m\n\n# 检查是否有重复项\n./string_manager.sh -c -d\n```\n\n### 场景2：修复重复项\n```bash\n# 先检查重复项\n./string_manager.sh -c -v -d\n\n# 确认后自动修复\n./string_manager.sh -c -f\n```\n\n### 场景3：全面检查\n```bash\n# 全功能检查\n./string_manager.sh -a -f\n\n# 检查行号一致性\n./position_check.sh\n```\n\n### 场景4：开发新功能后\n```bash\n# 添加新字符串后，检查同步性\n./string_manager.sh -m\n\n# 检查是否有重复\n./string_manager.sh -c -d\n```\n\n## ⚠️ 注意事项\n\n### 备份建议\n- 使用 `-f` 选项自动修复前，建议先运行 `-d` 选项检查\n- 脚本会自动创建备份文件（除非使用 `-n` 选项）\n- 备份文件格式：`原文件名.backup.时间戳`\n\n### 文件路径\n- 脚本默认使用 `src/main/resources/strings/` 目录下的文件\n- 可以指定其他文件路径作为参数\n- 确保文件路径正确且文件存在\n\n### 权限要求\n- 脚本需要执行权限：`chmod +x string_manager.sh position_check.sh`\n- 修改文件需要写入权限\n\n## 🔧 故障排除\n\n### 常见错误\n\n#### 1. 权限错误\n```bash\n# 解决方案：添加执行权限\nchmod +x string_manager.sh position_check.sh\n```\n\n#### 2. 文件不存在\n```bash\n# 检查文件路径\nls -la src/main/resources/strings/\n```\n\n#### 3. 语法错误\n```bash\n# 检查脚本语法\nbash -n string_manager.sh\nbash -n position_check.sh\n```\n\n### 调试技巧\n\n#### 1. 详细输出\n```bash\n# 使用 -v 选项查看详细信息\n./string_manager.sh -c -v -d\n```\n\n#### 2. 干运行模式\n```bash\n# 使用 -d 选项不修改文件\n./string_manager.sh -c -d\n```\n\n#### 3. 检查特定文件\n```bash\n# 指定文件路径\n./string_manager.sh -c /path/to/your/file.xml\n```\n\n## 📚 相关文档\n\n- [i18n 模块 README](README.md)\n- [字符串管理指南](I18N_STRING_MANAGEMENT_GUIDE.md)\n- [国际化测试文档](src/test/kotlin/cn/netdiscovery/monica/i18n/InternationalizationTest.kt)\n\n## 🤝 贡献指南\n\n如果你发现脚本的问题或有改进建议，请：\n\n1. 检查脚本语法：`bash -n script_name.sh`\n2. 测试功能：使用 `-d` 选项进行干运行\n3. 提交问题或改进建议\n\n---\n\n**最后更新**: 2024年12月\n**版本**: 2.0\n**维护者**: AI Assistant\n"
  },
  {
    "path": "i18n/build.gradle.kts",
    "content": "plugins {\n    kotlin(\"jvm\")\n}\n\nrepositories {\n    mavenCentral()\n}\n\njava {\n    toolchain {\n        languageVersion.set(JavaLanguageVersion.of(17))\n    }\n}\n\ndependencies {\n    testImplementation(kotlin(\"test\"))\n    testImplementation(\"junit:junit:4.13.2\")\n    implementation (\"org.jetbrains.kotlin:kotlin-stdlib\")\n\n    // log config\n    implementation(\"ch.qos.logback:logback-classic:${rootProject.extra[\"logback\"]}\")\n    implementation(\"ch.qos.logback:logback-core:${rootProject.extra[\"logback\"]}\")\n    implementation(\"ch.qos.logback:logback-access:${rootProject.extra[\"logback\"]}\")\n    \n    // Config module\n    implementation(project(\":config\"))\n}\n\ntasks.test {\n    useJUnitPlatform()\n}"
  },
  {
    "path": "i18n/position_check.sh",
    "content": "#!/bin/bash\n\n# 位置比对脚本\n# 检查中英文配置文件中相同key的行号是否一致\n\n# 颜色定义\nRED='\\033[0;31m'\nGREEN='\\033[0;32m'\nYELLOW='\\033[1;33m'\nNC='\\033[0m' # No Color\n\n# 文件路径\nZH_FILE=\"src/main/resources/strings/strings_zh.xml\"\nEN_FILE=\"src/main/resources/strings/strings_en.xml\"\n\necho \"=== 中英文字符串资源文件位置比对 ===\"\necho \"\"\n\n# 检查文件是否存在\nif [[ ! -f \"$ZH_FILE\" ]]; then\n    echo -e \"${RED}错误: 中文文件不存在: $ZH_FILE${NC}\"\n    exit 1\nfi\n\nif [[ ! -f \"$EN_FILE\" ]]; then\n    echo -e \"${RED}错误: 英文文件不存在: $EN_FILE${NC}\"\n    exit 1\nfi\n\n# 创建临时文件存储key和行号的映射\nZH_TEMP=$(mktemp)\nEN_TEMP=$(mktemp)\n\necho \"1. 提取中文文件中的key和行号...\"\ngrep -n 'name=\"[^\"]*\"' \"$ZH_FILE\" | sed 's/^\\([0-9]*\\):.*name=\"\\([^\"]*\\)\".*/\\1:\\2/' > \"$ZH_TEMP\"\n\necho \"2. 提取英文文件中的key和行号...\"\ngrep -n 'name=\"[^\"]*\"' \"$EN_FILE\" | sed 's/^\\([0-9]*\\):.*name=\"\\([^\"]*\\)\".*/\\1:\\2/' > \"$EN_TEMP\"\n\necho \"3. 统计信息...\"\nZH_COUNT=$(wc -l < \"$ZH_TEMP\")\nEN_COUNT=$(wc -l < \"$EN_TEMP\")\necho \"中文文件字符串数量: $ZH_COUNT\"\necho \"英文文件字符串数量: $EN_COUNT\"\necho \"\"\n\n# 找出共同的key\nCOMMON_KEYS=$(comm -12 <(cut -d: -f2 \"$ZH_TEMP\" | sort) <(cut -d: -f2 \"$EN_TEMP\" | sort))\nCOMMON_COUNT=$(echo \"$COMMON_KEYS\" | wc -l)\necho \"4. 共同key数量: $COMMON_COUNT\"\necho \"\"\n\n# 检查行号差异\necho \"5. 检查行号一致性...\"\nMISMATCHED_COUNT=0\nTOTAL_DIFF=0\nMAX_DIFF=0\n\necho \"$COMMON_KEYS\" | while read key; do\n    ZH_LINE=$(grep \":$key$\" \"$ZH_TEMP\" | cut -d: -f1)\n    EN_LINE=$(grep \":$key$\" \"$EN_TEMP\" | cut -d: -f1)\n    \n    if [[ -n \"$ZH_LINE\" && -n \"$EN_LINE\" ]]; then\n        DIFF=$((ZH_LINE - EN_LINE))\n        # 取绝对值\n        if [[ $DIFF -lt 0 ]]; then\n            ABS_DIFF=$((-DIFF))\n        else\n            ABS_DIFF=$DIFF\n        fi\n        \n        if [[ $ABS_DIFF -gt 0 ]]; then\n            echo -e \"${YELLOW}行号不匹配: $key${NC}\"\n            echo \"  中文文件第${ZH_LINE}行\"\n            echo \"  英文文件第${EN_LINE}行\"\n            echo \"  差异: $DIFF 行\"\n            echo \"\"\n        fi\n    fi\ndone\n\n# 统计不匹配的数量\nACTUAL_MISMATCHED=$(echo \"$COMMON_KEYS\" | while read key; do\n    ZH_LINE=$(grep \":$key$\" \"$ZH_TEMP\" | cut -d: -f1)\n    EN_LINE=$(grep \":$key$\" \"$EN_TEMP\" | cut -d: -f1)\n    \n    if [[ -n \"$ZH_LINE\" && -n \"$EN_LINE\" ]]; then\n        DIFF=$((ZH_LINE - EN_LINE))\n        # 取绝对值\n        if [[ $DIFF -lt 0 ]]; then\n            ABS_DIFF=$((-DIFF))\n        else\n            ABS_DIFF=$DIFF\n        fi\n        \n        if [[ $ABS_DIFF -gt 0 ]]; then\n            echo \"1\"\n        fi\n    fi\ndone | wc -l)\n\n# 计算总差异\nACTUAL_TOTAL_DIFF=$(echo \"$COMMON_KEYS\" | while read key; do\n    ZH_LINE=$(grep \":$key$\" \"$ZH_TEMP\" | cut -d: -f1)\n    EN_LINE=$(grep \":$key$\" \"$EN_TEMP\" | cut -d: -f1)\n    \n    if [[ -n \"$ZH_LINE\" && -n \"$EN_LINE\" ]]; then\n        DIFF=$((ZH_LINE - EN_LINE))\n        # 取绝对值\n        if [[ $DIFF -lt 0 ]]; then\n            ABS_DIFF=$((-DIFF))\n        else\n            ABS_DIFF=$DIFF\n        fi\n        echo \"$ABS_DIFF\"\n    fi\ndone | awk '{sum+=$1} END {print sum}')\n\n# 计算最大差异\nACTUAL_MAX_DIFF=$(echo \"$COMMON_KEYS\" | while read key; do\n    ZH_LINE=$(grep \":$key$\" \"$ZH_TEMP\" | cut -d: -f1)\n    EN_LINE=$(grep \":$key$\" \"$EN_TEMP\" | cut -d: -f1)\n    \n    if [[ -n \"$ZH_LINE\" && -n \"$EN_LINE\" ]]; then\n        DIFF=$((ZH_LINE - EN_LINE))\n        # 取绝对值\n        if [[ $DIFF -lt 0 ]]; then\n            ABS_DIFF=$((-DIFF))\n        else\n            ABS_DIFF=$DIFF\n        fi\n        echo \"$ABS_DIFF\"\n    fi\ndone | sort -n | tail -1)\n\necho \"6. 位置比对结果:\"\nif [[ $ACTUAL_MISMATCHED -eq 0 ]]; then\n    echo -e \"${GREEN}✅ 所有共同key的行号都一致！${NC}\"\nelse\n    echo -e \"${YELLOW}⚠️  发现 $ACTUAL_MISMATCHED 个key的行号不一致${NC}\"\n    echo \"  最大行号差异: $ACTUAL_MAX_DIFF 行\"\n    if [[ $ACTUAL_MISMATCHED -gt 0 ]]; then\n        AVERAGE_DIFF=$(echo \"scale=1; $ACTUAL_TOTAL_DIFF / $ACTUAL_MISMATCHED\" | bc 2>/dev/null || echo \"N/A\")\n        echo \"  平均行号差异: $AVERAGE_DIFF 行\"\n    fi\nfi\n\n# 清理临时文件\nrm \"$ZH_TEMP\" \"$EN_TEMP\"\n\necho \"\"\necho \"=== 位置比对完成 ===\""
  },
  {
    "path": "i18n/quick_check.sh",
    "content": "#!/bin/bash\n\n# i18n 文件快速检查脚本\n# 功能：一键检查中英文文件同步性、重复项和行号一致性\n\n# 颜色定义\nRED='\\033[0;31m'\nGREEN='\\033[0;32m'\nYELLOW='\\033[1;33m'\nBLUE='\\033[0;34m'\nNC='\\033[0m' # No Color\n\necho -e \"${BLUE}=== i18n 文件快速检查 ===${NC}\"\necho \"\"\n\n# 检查脚本是否存在\nif [[ ! -f \"string_manager.sh\" ]]; then\n    echo -e \"${RED}错误: string_manager.sh 不存在${NC}\"\n    exit 1\nfi\n\nif [[ ! -f \"position_check.sh\" ]]; then\n    echo -e \"${RED}错误: position_check.sh 不存在${NC}\"\n    exit 1\nfi\n\n# 检查脚本权限\nif [[ ! -x \"string_manager.sh\" ]]; then\n    echo -e \"${YELLOW}警告: string_manager.sh 没有执行权限，正在修复...${NC}\"\n    chmod +x string_manager.sh\nfi\n\nif [[ ! -x \"position_check.sh\" ]]; then\n    echo -e \"${YELLOW}警告: position_check.sh 没有执行权限，正在修复...${NC}\"\n    chmod +x position_check.sh\nfi\n\necho -e \"${BLUE}1. 检查中英文文件同步性...${NC}\"\necho \"----------------------------------------\"\n./string_manager.sh -m\necho \"\"\n\necho -e \"${BLUE}2. 检查重复项...${NC}\"\necho \"----------------------------------------\"\n./string_manager.sh -c -d\necho \"\"\n\necho -e \"${BLUE}3. 检查行号一致性...${NC}\"\necho \"----------------------------------------\"\n./position_check.sh\necho \"\"\n\necho -e \"${GREEN}=== 检查完成 ===${NC}\"\necho \"\"\necho -e \"${YELLOW}💡 提示:${NC}\"\necho \"  - 如果发现重复项，使用: ./string_manager.sh -c -f\"\necho \"  - 如果发现缺失翻译，请手动添加\"\necho \"  - 行号不一致是正常的，不影响功能\"\necho \"\"\necho -e \"${BLUE}📚 更多信息请查看: SCRIPT_USAGE_GUIDE.md${NC}\"\n"
  },
  {
    "path": "i18n/src/main/kotlin/cn/netdiscovery/monica/i18n/Language.kt",
    "content": "package cn.netdiscovery.monica.i18n\n\n/**\n * 支持的语言枚举\n */\nenum class Language(val code: String, val displayName: String, val flag: String) {\n    CHINESE(\"zh\", \"中文\", \"🇨🇳\"),\n    ENGLISH(\"en\", \"English\", \"🇺🇸\");\n    \n    companion object {\n        fun fromCode(code: String): Language {\n            return values().find { it.code == code } ?: CHINESE\n        }\n        \n        fun getSystemLanguage(): Language {\n            val systemLang = java.util.Locale.getDefault().language\n            return when (systemLang) {\n                \"zh\" -> CHINESE\n                \"en\" -> ENGLISH\n                else -> CHINESE // 默认中文，因为项目主要面向中文用户\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "i18n/src/main/kotlin/cn/netdiscovery/monica/i18n/LocalizationManager.kt",
    "content": "package cn.netdiscovery.monica.i18n\n\nimport cn.netdiscovery.monica.config.category.ConfigCategoryManager\n\n/**\n * 国际化管理器\n *\n * 负责管理应用的语言设置和本地化资源\n */\nobject LocalizationManager {\n    private const val LANGUAGE_KEY = \"selected_language\"\n\n    // 当前语言状态\n    private var _currentLanguage = getSavedLanguage()\n    val currentLanguage: Language\n        get() = _currentLanguage\n\n    // 语言变化监听器列表\n    private val languageChangeListeners = mutableListOf<() -> Unit>()\n\n    /**\n     * 添加语言变化监听器\n     */\n    fun addLanguageChangeListener(listener: () -> Unit) {\n        languageChangeListeners.add(listener)\n    }\n\n    /**\n     * 移除语言变化监听器\n     */\n    fun removeLanguageChangeListener(listener: () -> Unit) {\n        languageChangeListeners.remove(listener)\n    }\n\n    /**\n     * 获取保存的语言设置\n     */\n    private fun getSavedLanguage(): Language {\n        val savedCode: String? = ConfigCategoryManager.load(LANGUAGE_KEY, null as String?)\n        return if (savedCode != null) {\n            Language.fromCode(savedCode)\n        } else {\n            Language.getSystemLanguage()\n        }\n    }\n\n    /**\n     * 设置当前语言\n     */\n    fun setLanguage(language: Language) {\n        if (_currentLanguage != language) {\n            _currentLanguage = language\n            ConfigCategoryManager.save(LANGUAGE_KEY, language.code)\n            // 清除缓存，强制重新加载资源\n            clearCache()\n            // 通知所有监听器语言已变化\n            languageChangeListeners.forEach { it.invoke() }\n        }\n    }\n\n    /**\n     * 清除资源缓存\n     */\n    private fun clearCache() {\n        chineseXmlResource = null\n        englishXmlResource = null\n    }\n\n    // XML资源缓存\n    private var chineseXmlResource: XmlBasedStringResource? = null\n    private var englishXmlResource: XmlBasedStringResource? = null\n\n    /**\n     * 获取XML字符串资源\n     */\n    fun getXmlResource(language: Language): XmlBasedStringResource {\n        return when (language) {\n            Language.CHINESE -> {\n                if (chineseXmlResource == null) {\n                    chineseXmlResource = XmlBasedStringResource(Language.CHINESE)\n                }\n                chineseXmlResource ?: throw IllegalStateException(\"中文资源文件未加载\")\n            }\n            Language.ENGLISH -> {\n                if (englishXmlResource == null) {\n                    englishXmlResource = XmlBasedStringResource(Language.ENGLISH)\n                }\n                englishXmlResource ?: throw IllegalStateException(\"英文资源文件未加载\")\n            }\n        }\n    }\n\n    /**\n     * 获取当前语言的字符串资源\n     */\n    fun getString(key: String): String {\n        val xmlResource = getXmlResource(_currentLanguage)\n        return xmlResource.get(key)\n    }\n\n    /**\n     * 获取带参数的字符串资源\n     */\n    fun getString(key: String, vararg args: Any): String {\n        val xmlResource = getXmlResource(_currentLanguage)\n        return xmlResource.get(key, *args)\n    }\n\n    /**\n     * 获取所有支持的语言\n     */\n    fun getSupportedLanguages(): List<Language> = Language.values().toList()\n\n    /**\n     * 获取当前语言代码\n     */\n    fun getCurrentLanguageCode(): String = _currentLanguage.code\n\n    /**\n     * 获取当前语言显示名称\n     */\n    fun getCurrentLanguageDisplayName(): String = _currentLanguage.displayName\n}\n\n/**\n * 获取当前语言的字符串资源\n */\nfun getCurrentStringResource(): StringResource {\n    return StringResource(LocalizationManager.currentLanguage)\n}\n\n/**\n * 字符串资源访问器\n */\nclass StringResource(private val language: Language) {\n    private val xmlResource by lazy { LocalizationManager.getXmlResource(language) }\n\n    fun get(key: String): String = LocalizationManager.getString(key)\n    fun get(key: String, vararg args: Any): String = LocalizationManager.getString(key, *args)\n\n    /**\n     * 直接从XML资源获取字符串（用于测试和调试）\n     */\n    fun getFromXml(key: String): String = xmlResource.get(key)\n\n    /**\n     * 检查XML资源中是否包含指定key\n     */\n    fun containsInXml(key: String): Boolean = xmlResource.contains(key)\n\n    /**\n     * 获取XML资源信息\n     */\n    fun getXmlResourceInfo(): String = xmlResource.getResourceInfo()\n\n    /**\n     * 获取所有可用的键\n     */\n    fun getAllKeys(): Set<String> = xmlResource.getAllKeys()\n}\n"
  },
  {
    "path": "i18n/src/main/kotlin/cn/netdiscovery/monica/i18n/XmlStringResource.kt",
    "content": "package cn.netdiscovery.monica.i18n\n\nimport org.w3c.dom.Document\nimport org.w3c.dom.Element\nimport java.io.InputStream\nimport org.slf4j.LoggerFactory\nimport javax.xml.parsers.DocumentBuilderFactory\n\n/**\n * XML格式的字符串资源加载器\n * \n * 支持从XML文件加载国际化字符串资源\n */\nobject XmlStringResource {\n    private val logger = LoggerFactory.getLogger(XmlStringResource::class.java.name)\n    \n    /**\n     * 从XML文件加载字符串资源\n     */\n    fun loadStrings(resourcePath: String): Map<String, String> {\n        return try {\n            val inputStream: InputStream? = this::class.java.classLoader.getResourceAsStream(resourcePath)\n            if (inputStream == null) {\n                logger.warn(\"无法找到资源文件: $resourcePath\")\n                return emptyMap()\n            }\n            \n            val documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder()\n            val document: Document = documentBuilder.parse(inputStream)\n            \n            val stringMap = mutableMapOf<String, String>()\n            val stringNodes = document.getElementsByTagName(\"string\")\n            \n            for (i in 0 until stringNodes.length) {\n                val stringNode = stringNodes.item(i) as Element\n                val name = stringNode.getAttribute(\"name\")\n                val value = stringNode.textContent\n                \n                if (name.isNotEmpty() && value.isNotEmpty()) {\n                    stringMap[name] = value\n                } else {\n                    logger.warn(\"跳过无效的字符串资源: name='$name', value='$value'\")\n                }\n            }\n            \n            logger.info(\"成功加载 ${stringMap.size} 个字符串资源从: $resourcePath\")\n            stringMap\n            \n        } catch (e: Exception) {\n            logger.error(\"加载XML字符串资源失败: $resourcePath, 错误: ${e.message}\")\n            e.printStackTrace()\n            emptyMap()\n        }\n    }\n    \n    /**\n     * 获取指定语言的字符串资源\n     */\n    fun getStringsForLanguage(language: Language): Map<String, String> {\n        val resourcePath = when (language) {\n            Language.CHINESE -> \"strings/strings_zh.xml\"\n            Language.ENGLISH -> \"strings/strings_en.xml\"\n        }\n        \n        return loadStrings(resourcePath)\n    }\n}\n\n/**\n * 基于XML的字符串资源实现\n */\nclass XmlBasedStringResource(\n    private val language: Language\n) {\n    \n    private val strings: Map<String, String> = XmlStringResource.getStringsForLanguage(language)\n    private val logger = LoggerFactory.getLogger(XmlBasedStringResource::class.java.name)\n    \n    fun get(key: String): String {\n        val value = strings[key]\n        if (value == null) {\n            logger.warn(\"未找到字符串资源: $key (语言: ${language.name})\")\n            return \"[$key]\" // 返回带方括号的key作为fallback\n        }\n        return value\n    }\n    \n    /**\n     * 获取带参数替换的字符串\n     */\n    fun get(key: String, vararg args: Any): String {\n        val template = get(key)\n        return try {\n            String.format(template, *args)\n        } catch (e: Exception) {\n            logger.warn(\"字符串格式化失败: $key, 模板: '$template', 参数: ${args.contentToString()}\")\n            template\n        }\n    }\n    \n    /**\n     * 检查是否包含指定的key\n     */\n    fun contains(key: String): Boolean {\n        return strings.containsKey(key)\n    }\n    \n    /**\n     * 获取所有可用的keys\n     */\n    fun getAllKeys(): Set<String> {\n        return strings.keys\n    }\n    \n    /**\n     * 获取资源统计信息\n     */\n    fun getResourceInfo(): String {\n        return \"语言: ${language.name}, 字符串数量: ${strings.size}\"\n    }\n}\n"
  },
  {
    "path": "i18n/src/main/resources/strings/strings_en.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<resources>\n    <!-- Main window and menu -->\n    <string name=\"app_name\">Monica</string>\n    <string name=\"app_description\">Monica is a cross-platform image editor</string>\n    <string name=\"software_version_info\">Software Version Info</string>\n    <string name=\"open_local_image\">Open Local Image</string>\n    <string name=\"load_network_image\">Load Network Image</string>\n    <string name=\"save_image\">Save Image</string>\n    <string name=\"screenshot_full_screen\">Screenshot (Full Screen)</string>\n    <string name=\"screenshot_area\">Screenshot (Area Selection)</string>\n    <string name=\"web_screenshot\">Web Long Screenshot</string>\n    <string name=\"exit\">Exit</string>\n    \n    <!-- Control panel -->\n    <string name=\"control_panel\">Control Panel</string>\n    <string name=\"general_settings\">General Settings</string>\n    <string name=\"basic_functions\">Basic Functions</string>\n    <string name=\"color_correction\">Color Correction</string>\n    <string name=\"filter_effects\">Filter Effects</string>\n    <string name=\"ai_laboratory\">AI Laboratory</string>\n    \n    <!-- General settings -->\n    <string name=\"settings\">Settings</string>\n    <string name=\"monica_general_settings\">Monica General Settings</string>\n    <string name=\"update\">Update</string>\n    <string name=\"close\">Close</string>\n    \n    <!-- Basic functions -->\n    <string name=\"image_blur\">Image Blur</string>\n    <string name=\"image_mosaic\">Image Mosaic</string>\n    <string name=\"image_doodle\">Image Doodle</string>\n    <string name=\"shape_drawing\">Shape Drawing</string>\n    <string name=\"color_picker\">Color Picker</string>\n    <string name=\"crop_image\">Crop Image</string>\n    <string name=\"image_compression\">Image Compression</string>\n    <string name=\"original\">Original</string>\n    <string name=\"compressed\">Compressed</string>\n    <string name=\"input_selection\">Input Selection</string>\n    <string name=\"single_image\">Single Image</string>\n    <string name=\"batch_folder\">Folder (Batch)</string>\n    <string name=\"generate_gif\">Generate GIF</string>\n    \n    <!-- Image Compression Related -->\n    <string name=\"compression_mode\">Compression Mode</string>\n    <string name=\"compression_algorithm\">Compression Algorithm</string>\n    <string name=\"quality_setting\">Quality Setting (Visually Lossless)</string>\n    <string name=\"quality_description\">Higher values result in lower compression rates and larger files; lower values result in higher compression rates but may lose details</string>\n    <string name=\"compression_level\">Compression Level</string>\n    <string name=\"compression_level_description\">Higher levels result in higher compression rates but longer processing times; lower levels are faster to process</string>\n    <string name=\"compress_image\">Compress Image</string>\n    <string name=\"select_input_folder\">Select Input Folder</string>\n    <string name=\"select_output_folder\">Select Output Folder</string>\n    <string name=\"start_batch_compression\">Start Batch Compression</string>\n    <string name=\"compression_result\">Compression Result</string>\n    <string name=\"original_size\">Original Size</string>\n    <string name=\"compressed_size\">Compressed Size</string>\n    <string name=\"compression_ratio\">Compression Ratio</string>\n    <string name=\"size_increase\">Increase</string>\n    <string name=\"compressed_file_larger_warning\">Tip: The compressed file is larger than the original. Try lowering quality or choosing another algorithm (e.g., JPEG Quality for photos; PNG Optimization for screenshots/transparency).</string>\n    <string name=\"selected\">Selected</string>\n    <string name=\"error_please_select_image\">Error: Please select an image first</string>\n    <string name=\"compressing_image\">Compressing image...</string>\n    <string name=\"webp_not_supported\">WebP format is not supported on this system, automatically converted to %s</string>\n    <string name=\"webp_encode_failed\">WebP encoding failed, automatically converted to %s</string>\n    <string name=\"compression_success\">Compression successful!</string>\n    <string name=\"compression_failed\">Compression failed</string>\n    <string name=\"compression_error\">Compression error: %s</string>\n    <string name=\"preparing_batch_compression\">Preparing batch compression...</string>\n    <string name=\"no_images_in_folder\">No images in folder</string>\n    <string name=\"compression_cancelled\">Compression cancelled</string>\n    <string name=\"compressing_file\">Compressing: %s (%d/%d)</string>\n    <string name=\"cannot_read_image\">Cannot read image</string>\n    <string name=\"batch_compression_completed\">Batch compression completed (Success: %d/%d)</string>\n    <string name=\"batch_compression_error\">Batch compression error: %s</string>\n    <string name=\"please_select_image_to_compress\">Please select an image to compress</string>\n    <string name=\"select_image\">Select Image</string>\n    <string name=\"please_select_or_load_image\">Please select or load an image first</string>\n    <string name=\"file_overwrite_confirm\">File Overwrite Confirmation</string>\n    <string name=\"file_exists_overwrite\">File \\\"%s\\\" already exists. Overwrite?</string>\n    <string name=\"save_compressed_image\">Save Compressed Image</string>\n    <string name=\"please_compress_first\">Please compress first</string>\n    <string name=\"start_compression\">Start Compression</string>\n    <string name=\"please_select_image_in_preview\">Please select an image in the preview area first</string>\n    <string name=\"batch_mode_no_preview\">Batch mode does not support preview comparison. Please check progress and summary.</string>\n    <string name=\"reset\">Reset</string>\n    <string name=\"applied_to_editor\">Applied to editor</string>\n    <string name=\"apply_to_editor\">Apply to Editor</string>\n    <string name=\"save_success\">Save successful: %s</string>\n    <string name=\"save_failed\">Save failed</string>\n    <string name=\"save\">Save</string>\n    <string name=\"cannot_read_image_file\">Cannot read image file</string>\n    <string name=\"load_image_failed\">Failed to load image: %s</string>\n    <string name=\"image_cleared\">Image cleared</string>\n    <string name=\"clear_image\">Clear image</string>\n    <string name=\"webp_not_supported_auto_convert\">WebP is not supported on this system, will automatically convert to %s</string>\n    <string name=\"batch_compression_warning\">Batch Compression Warning</string>\n    <string name=\"output_folder_has_files\">Output folder already contains %d files. Batch compression may overwrite files with the same name. Continue?</string>\n    <string name=\"cancel\">Cancel</string>\n    <string name=\"undo\">Undo</string>\n    <string name=\"undo_not_available\">Nothing to undo</string>\n    <string name=\"undo_success\">Undone</string>\n    <string name=\"undo_and_reset_success\">Undone and reset</string>\n    <string name=\"reset_done\">Reset</string>\n    <string name=\"format_conversion_warning_jpg_to_png\">Note: Converting JPG to PNG may result in a larger file size, as PNG is a lossless format</string>\n    <string name=\"format_conversion_warning_jpg_to_webp_lossless\">Note: Converting JPG to WebP Lossless may result in a larger file size</string>\n    \n    <!-- Shape drawing -->\n    <string name=\"select_color\">Select Color</string>\n    <string name=\"change_properties\">Change Properties</string>\n    <string name=\"line\">Line</string>\n    <string name=\"circle\">Circle</string>\n    <string name=\"triangle\">Triangle</string>\n    <string name=\"rectangle\">Rectangle</string>\n    <string name=\"polygon\">Polygon</string>\n    <string name=\"add_text\">Add Text</string>\n    <string name=\"save\">Save</string>\n    \n    <!-- Doodle function -->\n    <string name=\"brush\">Brush</string>\n    <string name=\"eraser\">Eraser</string>\n    <string name=\"clear\">Clear</string>\n    \n    <!-- Color correction -->\n    <string name=\"natural_language_color\">Natural Language Color</string>\n    <string name=\"enter_color_instruction\">Enter color instruction</string>\n    <string name=\"color_parameters_updated\">Parameters updated: %s</string>\n    \n    <!-- Filter effects -->\n    <string name=\"filter_parameters\">Filter Parameters</string>\n    \n    <!-- AI Laboratory -->\n    <string name=\"ai_laboratory_description\">AI Laboratory</string>\n    <string name=\"simple_cv_algorithm\">Simple CV Algorithm Quick Validation</string>\n    <string name=\"face_detection\">Face Detection</string>\n    <string name=\"generate_sketch\">Generate Sketch</string>\n    <string name=\"face_swap\">Face Swap</string>\n    <string name=\"anime_style\">Anime Style</string>\n    \n    <!-- AI experiment pages -->\n    <string name=\"home\">Home</string>\n    <string name=\"binary_image\">Binary Image</string>\n    <string name=\"edge_detection\">Edge Detection</string>\n    <string name=\"contour_analysis\">Contour Analysis</string>\n    <string name=\"image_enhance\">Image Enhancement</string>\n    <string name=\"image_denoising\">Image Denoising</string>\n    <string name=\"morphological_operations\">Morphological Operations</string>\n    <string name=\"match_template\">Template Matching</string>\n    <string name=\"parameter_history\">Parameter History</string>\n    \n    <!-- Crop function -->\n    <string name=\"crop_type\">Crop Type</string>\n    <string name=\"content_scale\">Content Scale</string>\n    <string name=\"aspect_ratio\">Aspect Ratio</string>\n    <string name=\"crop_frame\">Crop Frame</string>\n    <string name=\"crop_properties_settings\">Crop Properties Settings</string>\n    <string name=\"confirm_crop\">Confirm</string>\n    <string name=\"dismiss_crop\">Dismiss</string>\n    \n    <!-- Dialogs and prompts -->\n    <string name=\"loading\">Loading...</string>\n    <string name=\"error\">Error</string>\n    <string name=\"success\">Success</string>\n    <string name=\"warning\">Warning</string>\n    <string name=\"info\">Info</string>\n    \n    <!-- Property settings -->\n    <string name=\"alpha\">Alpha</string>\n    <string name=\"font_size\">Font Size</string>\n    <string name=\"fill\">Fill</string>\n    <string name=\"border\">Border</string>\n    <string name=\"stroke_width\">Stroke Width</string>\n    \n    <!-- Network image loading -->\n    <string name=\"load_network_image_dialog\">Load Network Image</string>\n    <string name=\"enter_image_url\">Enter image URL</string>\n    <string name=\"load\">Load</string>\n    <string name=\"url_invalid\">Invalid URL format</string>\n\n    <!-- Theme Settings -->\n    <string name=\"basic_settings\">Basic Settings</string>\n    <string name=\"api_settings\">API Settings</string>\n    <string name=\"theme_settings\">Theme Settings</string>\n    <string name=\"language_settings\">Language Settings</string>\n    <string name=\"current_theme\">Current Theme</string>\n    <string name=\"select_theme\">Select Theme</string>\n    <string name=\"theme_light\">Light Theme</string>\n    <string name=\"theme_dark\">Dark Theme</string>\n    <string name=\"theme_blue\">Blue Theme</string>\n    <string name=\"theme_green\">Green Theme</string>\n    <string name=\"theme_purple\">Purple Theme</string>\n    <string name=\"theme_orange\">Orange Theme</string>\n    <string name=\"theme_pink\">Pink Theme</string>\n    <string name=\"reset_to_default_theme\">Reset to Default Theme</string>\n    \n    <!-- Version info -->\n    <string name=\"version_info\">Version Info</string>\n    <string name=\"copyright\">© 2024 Tony Shen. All rights reserved.</string>\n    \n    <!-- Status and messages -->\n    <string name=\"basic_function_cancelled\">Basic functions cancelled</string>\n    <string name=\"basic_function_selected\">Basic functions selected</string>\n    <string name=\"general_settings_cancelled\">General settings cancelled</string>\n    <string name=\"general_settings_selected\">General settings selected</string>\n    <string name=\"color_correction_cancelled\">Color correction cancelled</string>\n    <string name=\"color_correction_selected\">Color correction selected</string>\n    <string name=\"filter_cancelled\">Filter effects cancelled</string>\n    <string name=\"filter_selected\">Filter effects selected</string>\n    <string name=\"ai_laboratory_cancelled\">AI laboratory cancelled</string>\n    <string name=\"ai_laboratory_selected\">AI laboratory selected</string>\n    \n    <!-- Tooltips -->\n    <string name=\"simple_cv_tooltip\">Simple CV Algorithm Quick Validation</string>\n    <string name=\"face_detect_tooltip\">Face Detection</string>\n    <string name=\"sketch_drawing_tooltip\">Generate Sketch</string>\n    <string name=\"face_swap_tooltip\">Face Swap</string>\n    <string name=\"anime_style_tooltip\">Anime Style</string>\n    \n    <!-- Version info -->\n    <string name=\"monica_software_info\">Monica Software Information</string>\n    <string name=\"monica_version_info\">Monica Version: %s, %s, Build Time: %s</string>\n    <string name=\"opencv_version_info\">OpenCV Version: %s, Local Algorithm Library: %s</string>\n    <string name=\"copyright_info\">Copyright: Copyright 2024-Present, Tony Shen</string>\n    <string name=\"github_url\">Github URL: https://github.com/fengzhizi715/Monica</string>\n    \n    <!-- Settings related -->\n    <string name=\"output_box_color_settings\">Output Box Color Settings:</string>\n    <string name=\"area_size_settings\">Area Size Settings (for blur/mosaic):</string>\n    <string name=\"max_history_size\">Max History Size per Module:</string>\n    <string name=\"algorithm_service_url\">Algorithm Service URL:</string>\n    <string name=\"init_filter_params\">Initialize filter parameter configuration</string>\n    <string name=\"clear_cache_data\">Clear all cache data</string>\n    <string name=\"reset_to_system_language\">Reset</string>\n    \n    <!-- Basic functions -->\n    <string name=\"x_direction\">X Direction</string>\n    <string name=\"y_direction\">Y Direction</string>\n    \n    <!-- Filter effects -->\n    <string name=\"enter_filter\">Enter Filter Interface</string>\n    <string name=\"select_filter\">Please select a filter</string>\n    <string name=\"filter_name\">%s Filter</string>\n    <string name=\"select_filter_first\">Please select a filter first</string>\n    <string name=\"image_editor_filter_module\">Image Editor: Filter Module</string>\n    <string name=\"export\">Export</string>\n    <string name=\"adjustments\">Adjustments</string>\n    <string name=\"reset_filter\">Reset Filter</string>\n    <string name=\"apply\">Apply</string>\n    <string name=\"failed_to_apply_filter\">Failed to apply filter</string>\n    <string name=\"filter_applied\">Filter applied</string>\n    <string name=\"no_image\">No image</string>\n    <string name=\"no_filters_found\">No filters found</string>\n    <string name=\"param_summary\">Parameter Summary</string>\n    <string name=\"param_summary_default\">Using default parameters</string>\n    <string name=\"param_summary_changed_count\">%d adjusted</string>\n    <string name=\"param_summary_reset_hint\">Tip: click \"Reset Filter\" at the bottom to restore default parameters</string>\n    <string name=\"clear_filter\">Clear Filter</string>\n    <!-- ColorFilter style options -->\n    <string name=\"color_filter_style_0\">Autumn</string>\n    <string name=\"color_filter_style_1\">Bone</string>\n    <string name=\"color_filter_style_2\">Cool</string>\n    <string name=\"color_filter_style_3\">Warm</string>\n    <string name=\"color_filter_style_4\">HSV</string>\n    <string name=\"color_filter_style_5\">Jet</string>\n    <string name=\"color_filter_style_6\">Ocean</string>\n    <string name=\"color_filter_style_7\">Pink</string>\n    <string name=\"color_filter_style_8\">Rainbow</string>\n    <string name=\"color_filter_style_9\">Spring</string>\n    <string name=\"color_filter_style_10\">Summer</string>\n    <string name=\"color_filter_style_11\">Winter</string>\n    <!-- NatureFilter style options -->\n    <string name=\"nature_filter_style_1\">Atmosphere</string>\n    <string name=\"nature_filter_style_2\">Burning</string>\n    <string name=\"nature_filter_style_3\">Haze</string>\n    <string name=\"nature_filter_style_4\">Frozen</string>\n    <string name=\"nature_filter_style_5\">Lava</string>\n    <string name=\"nature_filter_style_6\">Metal</string>\n    <string name=\"nature_filter_style_7\">Ocean</string>\n    <string name=\"nature_filter_style_8\">Flowing Water</string>\n    <string name=\"notes\">Notes:</string>\n    <string name=\"search\">Search</string>\n    <string name=\"fit\">Fit</string>\n    \n    <!-- Anime style -->\n    <string name=\"select_anime_style\">Please select an anime style</string>\n    \n    <!-- GIF generation -->\n    <string name=\"add_image_first\">Please add images first</string>\n    <string name=\"select_images\">Select images</string>\n    <string name=\"gif_generation_strategy\">GIF Generation Strategy</string>\n    <string name=\"gif_width\">GIF Width</string>\n    <string name=\"gif_height\">GIF Height</string>\n    <string name=\"frame_interval\">Frame Interval (ms)</string>\n    <string name=\"loop_playback\">Loop Playback</string>\n    \n    <!-- Natural language color correction -->\n    <string name=\"natural_language_color_correction\">Natural Language Color Correction</string>\n    \n    <!-- AI experiment related -->\n    <string name=\"gaussian_filter\">Gaussian Filter</string>\n    <string name=\"median_filter\">Median Filter</string>\n    <string name=\"gaussian_bilateral_filter\">Gaussian Bilateral Filter</string>\n    <string name=\"mean_shift_filter\">Mean Shift Filter</string>\n    <string name=\"grayscale_image\">Grayscale Image</string>\n    <string name=\"threshold_segmentation\">Threshold Segmentation</string>\n    <string name=\"canny_edge_detection\">Canny Edge Detection</string>\n    <string name=\"color_image_segmentation\">Color Image Segmentation</string>\n    <string name=\"template\">Template</string>\n    <string name=\"matching_method\">Matching Method</string>\n    <string name=\"rotation\">Rotation</string>\n    <string name=\"min_angle\">Min Angle</string>\n    <string name=\"max_angle\">Max Angle</string>\n    <string name=\"angle_step\">Angle Step</string>\n    <string name=\"scale\">Scale</string>\n    <string name=\"min_scale\">Min Scale</string>\n    <string name=\"max_scale\">Max Scale</string>\n    <string name=\"scale_step\">Scale Step</string>\n    <string name=\"template_matching_params\">Template Matching Parameters</string>\n    <string name=\"threshold\">Threshold</string>\n    <string name=\"nms_params\">NMS Parameters</string>\n    <string name=\"score_threshold\">Score Threshold</string>\n    <string name=\"nms_threshold\">NMS Threshold</string>\n    <string name=\"filter_settings\">Filter Settings</string>\n    <string name=\"min_perimeter\">Min Value</string>\n    <string name=\"max_perimeter\">Max Value</string>\n    <string name=\"min_area\">Min Value</string>\n    <string name=\"max_area\">Max Value</string>\n    <string name=\"min_roundness\">Min Value</string>\n    <string name=\"max_roundness\">Max Value</string>\n    <string name=\"min_aspect_ratio\">Min Value</string>\n    <string name=\"max_aspect_ratio\">Max Value</string>\n    <string name=\"display_settings\">Display Settings</string>\n    <string name=\"operation_element\">Operation Element</string>\n    <string name=\"structural_element\">Structural Element</string>\n    <string name=\"width\">Width</string>\n    <string name=\"height\">Height</string>\n    <string name=\"histogram_equalization\">Histogram Equalization</string>\n    <string name=\"clahe\">Contrast Limited Adaptive Histogram Equalization (CLAHE)</string>\n    <string name=\"gamma_transform\">Gamma Transform</string>\n    <string name=\"laplace_sharpening\">Laplace Sharpening</string>\n    <string name=\"usm_sharpening\">USM Sharpening</string>\n    <string name=\"automatic_color_balance\">Automatic Color Balance</string>\n    <string name=\"edge_detection_operator\">Edge Detection Operator</string>\n    \n    <!-- Face swap -->\n    <string name=\"replace_target_face_count\">Replace face count in target</string>\n    \n    <!-- Common prompts -->\n    <string name=\"click_to_select_image\">Click to select image</string>\n    \n    <!-- Window titles -->\n    <string name=\"monica_image_editor\">Monica Image Editor</string>\n    <string name=\"notification\">Notification</string>\n    <string name=\"export_image\">Export Image</string>\n    \n    <!-- Common labels -->\n    <string name=\"min_value\">Min Value</string>\n    <string name=\"max_value\">Max Value</string>\n    \n    <string name=\"restore\">Restore</string>\n    <string name=\"restore_original\">Restore Original</string>\n    <string name=\"previous_step\">Previous Step</string>\n    <string name=\"enlarge_preview\">Enlarge Preview</string>\n    <string name=\"delete\">Delete</string>\n    \n    <!-- Image operations -->\n    <string name=\"image_color_correction\">Image Color Correction</string>\n    <string name=\"enter_image_color_correction\">Enter Image Color Correction Interface</string>\n    <string name=\"image_flip\">Image Flip</string>\n    <string name=\"image_rotate\">Image Rotate</string>\n    <string name=\"image_scale\">Image Scale</string>\n    <string name=\"image_shear\">Image Shear</string>\n    <string name=\"image_crop\">Image Crop</string>\n    \n    <!-- Anime styles -->\n    <string name=\"miyazaki_style\">Miyazaki Style</string>\n    <string name=\"japanese_portrait_style\">Japanese Portrait Style</string>\n    <string name=\"black_white_line_art\">Black &amp; White Line Art</string>\n    <string name=\"shinkai_style\">Shinkai Style</string>\n    <string name=\"cute_style\">Cute Style</string>\n    \n    <!-- Algorithm service -->\n    <string name=\"image_grayscale\">Image Grayscale</string>\n    <string name=\"threshold_type\">Threshold Type</string>\n    \n    <!-- Edge detection operators -->\n    <string name=\"prewitt_operator\">Prewitt Operator</string>\n    <string name=\"sobel_operator\">Sobel Operator</string>\n    <string name=\"log_operator\">LoG Operator</string>\n    <string name=\"dog_operator\">DoG Operator</string>\n    <string name=\"first_derivative_operator\">First Derivative Operator</string>\n    <string name=\"second_derivative_operator\">Second Derivative Operator</string>\n    <string name=\"first_derivative_edge_detection\">First Derivative Edge Detection</string>\n    <string name=\"second_derivative_edge_detection\">Second Derivative Edge Detection</string>\n    <string name=\"canny_operator\">Canny Operator</string>\n    <string name=\"canny_edge_detection_full\">Canny Edge Detection</string>\n    \n    <!-- Prompt messages -->\n    <string name=\"click_to_select_image_or_drag\">Click to select image or drag image here</string>\n    <string name=\"image_save_success\">Image saved successfully</string>\n    <string name=\"image_save_failed\">Image save failed</string>\n    <string name=\"please_select_first_derivative_operator\">Please select first derivative operator type</string>\n    <string name=\"please_select_second_derivative_operator\">Please select second derivative operator type</string>\n    <string name=\"please_select_threshold_type\">Please select threshold type</string>\n    <string name=\"please_select_global_threshold_segmentation\">Please select global threshold segmentation type</string>\n    <string name=\"please_select_adaptive_threshold_algorithm\">Please select adaptive threshold algorithm type</string>\n    <string name=\"please_select_threshold_type_and_segmentation\">Please select threshold type and global threshold segmentation or adaptive threshold segmentation</string>\n    \n    <!-- Parameter validation errors -->\n    <string name=\"ksize_needs_int\">ksize needs int type</string>\n    <string name=\"sigma_x_needs_double\">sigmaX needs double type</string>\n    <string name=\"sigma_y_needs_double\">sigmaY needs double type</string>\n    <string name=\"d_needs_int\">d needs int type</string>\n    <string name=\"sigma_color_needs_double\">sigmaColor needs double type</string>\n    <string name=\"sigma_space_needs_double\">sigmaSpace needs double type</string>\n    <string name=\"sp_needs_double\">sp needs double type</string>\n    <string name=\"sr_needs_double\">sr needs double type</string>\n    <string name=\"width_needs_int\">width needs int type</string>\n    <string name=\"height_needs_int\">height needs int type</string>\n    <string name=\"x_direction_needs_float\">x direction needs float type</string>\n    <string name=\"y_direction_needs_float\">y direction needs float type</string>\n    <string name=\"block_size_needs_int\">blockSize needs int type</string>\n    <string name=\"c_needs_int\">c needs int type</string>\n    <string name=\"threshold1_needs_double\">threshold1 needs double type</string>\n    <string name=\"threshold2_needs_double\">threshold2 needs double type</string>\n    <string name=\"aperture_size_needs_int\">apertureSize needs int type</string>\n    <string name=\"hmin_needs_int\">hmin needs int type</string>\n    <string name=\"smin_needs_int\">smin needs int type</string>\n    <string name=\"vmin_needs_int\">vmin needs int type</string>\n    <string name=\"hmax_needs_int\">hmax needs int type</string>\n    <string name=\"smax_needs_int\">smax needs int type</string>\n    <string name=\"vmax_needs_int\">vmax needs int type</string>\n    \n    <!-- Additional parameter validation errors -->\n    <string name=\"sigma1_needs_double\">sigma1 needs double type</string>\n    <string name=\"sigma2_needs_double\">sigma2 needs double type</string>\n    <string name=\"size_needs_int\">size needs int type</string>\n    \n    <!-- Natural language color correction -->\n    <string name=\"update_parameters\">🤖 Update Parameters:</string>\n    <string name=\"request_failed\">Request failed:</string>\n    <string name=\"unknown_error\">Unknown error</string>\n    <string name=\"contrast\">Contrast</string>\n    <string name=\"hue\">Hue</string>\n    <string name=\"saturation\">Saturation</string>\n    <string name=\"lightness\">Lightness</string>\n    <string name=\"temperature\">Temperature</string>\n    <string name=\"highlight\">Highlight</string>\n    <string name=\"shadow\">Shadow</string>\n    <string name=\"sharpen\">Sharpen</string>\n    <string name=\"corner\">Corner</string>\n    <string name=\"no_significant_changes\">No significant changes</string>\n    \n    <!-- AI Experiment pages -->\n    <string name=\"experiment_home_description\">This module uses OpenCV C++ algorithms and is currently only suitable for quick validation and parameter tuning of simple CV algorithms.</string>\n    \n    <!-- Experiment page navigation -->\n    <string name=\"experiment_home\">Home</string>\n    <string name=\"experiment_binary_image\">Binary Image</string>\n    <string name=\"experiment_edge_detection\">Edge Detection</string>\n    <string name=\"experiment_contour_analysis\">Contour Analysis</string>\n    <string name=\"experiment_image_enhance\">Image Enhancement</string>\n    <string name=\"experiment_image_denoising\">Image Denoising</string>\n    <string name=\"experiment_morphological_operations\">Morphological Operations</string>\n    <string name=\"experiment_match_template\">Template Matching</string>\n    <string name=\"experiment_history\">Parameter History</string>\n    \n    <!-- Binary image page -->\n    <string name=\"please_binarize_image_first\">Please binarize the current image first</string>\n    <string name=\"please_select_canny_operator\">Please select Canny operator</string>\n    \n    <!-- Contour analysis page -->\n    <string name=\"contour_filter_settings\">Contour Filter Settings</string>\n    <string name=\"contour_display_settings\">Contour Display Settings</string>\n    <string name=\"perimeter\">Perimeter</string>\n    <string name=\"area\">Area</string>\n    <string name=\"roundness\">Roundness</string>\n    <string name=\"show_original_image\">Show Original Image</string>\n    <string name=\"show_bounding_rect\">Show Bounding Rectangle</string>\n    <string name=\"show_center\">Show Center</string>\n    <string name=\"perimeter_max_needs_double\">Perimeter maximum needs double type</string>\n    <string name=\"area_min_needs_double\">Area minimum needs double type</string>\n    <string name=\"area_max_needs_double\">Area maximum needs double type</string>\n    <string name=\"roundness_min_needs_double\">Roundness minimum needs double type</string>\n    <string name=\"roundness_max_needs_double\">Roundness maximum needs double type</string>\n    <string name=\"aspect_ratio_min_needs_double\">Aspect ratio minimum needs double type</string>\n    <string name=\"aspect_ratio_max_needs_double\">Aspect ratio maximum needs double type</string>\n    <string name=\"perimeter_at_least_one_value\">Perimeter needs at least one minimum or maximum value</string>\n    <string name=\"area_at_least_one_value\">Area needs at least one minimum or maximum value</string>\n    <string name=\"roundness_at_least_one_value\">Roundness needs at least one minimum or maximum value</string>\n    <string name=\"aspect_ratio_at_least_one_value\">Aspect ratio needs at least one minimum or maximum value</string>\n    <string name=\"please_binarize_image_first_for_contour\">Please binarize the current image first</string>\n    \n    <string name=\"laplace_sharpen\">Laplace Sharpen</string>\n    <string name=\"usm_sharpen\">USM Sharpen</string>\n    <string name=\"auto_color_balance\">Auto Color Balance</string>\n    <string name=\"clip_limit_needs_double\">clipLimit needs double type</string>\n    <string name=\"size_needs_int_for_enhance\">size needs int type</string>\n    <string name=\"gamma_needs_float\">gamma needs float type</string>\n    <string name=\"radius_needs_int\">Radius needs int type</string>\n    <string name=\"threshold_needs_int\">Threshold needs int type</string>\n    <string name=\"amount_needs_int\">Amount needs int type</string>\n    <string name=\"ratio_needs_int\">Ratio needs int type</string>\n    \n    <!-- Morphological operations page -->\n    <string name=\"operating_elements\">Operating Elements</string>\n    <string name=\"structural_elements\">Structural Elements</string>\n    <string name=\"erosion\">Erosion</string>\n    <string name=\"dilation\">Dilation</string>\n    <string name=\"closing\">Closing</string>\n    <string name=\"morphological_gradient\">Morphological Gradient</string>\n    <string name=\"top_hat\">Top Hat</string>\n    <string name=\"black_hat\">Black Hat</string>\n    <string name=\"hit_miss\">Hit Miss</string>\n    <string name=\"cross\">Cross</string>\n    <string name=\"ellipse\">Ellipse</string>\n    <string name=\"width_needs_int_for_morph\">width needs int type</string>\n    <string name=\"height_needs_int_for_morph\">height needs int type</string>\n    \n    <!-- Template matching page -->\n    <string name=\"original_image_matching\">Original Image Matching</string>\n    <string name=\"grayscale_matching\">Grayscale Matching</string>\n    <string name=\"edge_matching\">Edge Matching</string>\n    <string name=\"import_template\">Import Template:</string>\n    <string name=\"delete_source_image\">Delete source image</string>\n    <string name=\"please_import_template_first\">Please import template file first</string>\n    <string name=\"angle_start_needs_int\">angleStart needs int type, and angleStart &gt;= 0</string>\n    <string name=\"angle_end_needs_int\">angleEnd needs int type, and angleEnd &lt;= 360</string>\n    <string name=\"angle_step_needs_int\">angleStep needs int type, and angleStep &gt; 0</string>\n    <string name=\"scale_start_needs_double\">scaleStart needs double type, and scaleStart &gt;= 0</string>\n    <string name=\"scale_end_needs_double\">scaleEnd needs double type, and scaleStart &lt;= 1.0</string>\n    <string name=\"scale_step_needs_double\">scaleStep needs double type, and scaleStep &gt; 0</string>\n    <string name=\"match_template_threshold_needs_double\">matchTemplateThreshold needs double type, and matchTemplateThreshold &gt;= 0</string>\n    <string name=\"score_threshold_needs_float\">scoreThreshold needs float type, and scoreThreshold &gt;= 0</string>\n    <string name=\"nms_threshold_needs_float\">nmsThreshold needs float type, and nmsThreshold &gt;= 0</string>\n    <string name=\"template_matching\">Template Matching</string>\n    \n    <!-- History page -->\n    <string name=\"operation\">Operation</string>\n    <string name=\"time\">Time</string>\n    <string name=\"parameters\">Parameters</string>\n    <string name=\"description\">Description</string>\n    \n    <!-- Log messages -->\n    <string name=\"threshold_type_cancelled\">Threshold type cancelled</string>\n    <string name=\"threshold_type_selected\">Threshold type selected</string>\n    <string name=\"global_threshold_cancelled\">Global threshold segmentation cancelled</string>\n    <string name=\"global_threshold_selected\">Global threshold segmentation selected</string>\n    <string name=\"adaptive_threshold_cancelled\">Adaptive threshold segmentation cancelled</string>\n    <string name=\"adaptive_threshold_selected\">Adaptive threshold segmentation selected</string>\n    <string name=\"opencv_debug_view_init\">OpenCVDebugView initialization on startup</string>\n    <string name=\"opencv_debug_view_dispose\">OpenCVDebugView resource cleanup on close</string>\n    \n    <!-- Image enhancement page button texts -->\n    <string name=\"histogram_equalization_button\">Histogram Equalization</string>\n    <string name=\"clahe_button\">CLAHE</string>\n    <string name=\"gamma_transform_button\">Gamma Transform</string>\n    <string name=\"laplace_sharpen_button\">Laplace Sharpen</string>\n    <string name=\"usm_sharpen_button\">USM Sharpen</string>\n    <string name=\"auto_color_balance_button\">Auto Color Balance</string>\n    \n    <!-- Missing translations -->\n    <string name=\"adaptive_threshold_algorithm\">Adaptive Threshold Algorithm</string>\n    <string name=\"adaptive_threshold_segmentation\">Adaptive Threshold Segmentation</string>\n    <string name=\"algorithm_service_error\">Algorithm Service Error</string>\n    <string name=\"global_threshold_segmentation\">Global Threshold Segmentation</string>\n    <string name=\"laplace_operator\">Laplace Operator</string>\n    <string name=\"opening\">Opening</string>\n    <string name=\"perimeter_min_needs_double\">Perimeter minimum needs double type</string>\n    <string name=\"please_select_image_first\">Please select image first</string>\n    <string name=\"roberts_operator\">Roberts Operator</string>\n    <string name=\"show_min_area_rect\">Show Minimum Area Rectangle</string>\n    \n    <!-- Gemini API Settings -->\n    <string name=\"gemini_api_key\">Gemini API Key</string>\n    <string name=\"gemini_api_key_title\">Gemini API Key</string>\n    <string name=\"gemini_api_key_description\">Gemini API key for natural language color correction</string>\n    <string name=\"ai_provider_selection\">AI Service Provider</string>\n    <string name=\"ai_provider_deepseek\">DeepSeek</string>\n    <string name=\"ai_provider_gemini\">Gemini</string>\n    <string name=\"select_ai_provider\">Select AI Service Provider</string>\n    \n    <!-- Language Settings -->\n    <string name=\"language_settings\">Language Settings</string>\n    <string name=\"current_language\">Current Language</string>\n    \n    <!-- Doodle Module -->\n    <string name=\"brush_settings\">Brush Settings</string>\n    <string name=\"clear_canvas\">Clear Canvas</string>\n    <string name=\"revoke\">Revoke</string>\n    <string name=\"stroke_cap\">Stroke Cap</string>\n    <string name=\"stroke_join\">Stroke Join</string>\n    \n    <!-- Common button texts -->\n    <string name=\"confirm\">Confirm</string>\n    <string name=\"cancel\">Cancel</string>\n    \n    <!-- Version info -->\n    <string name=\"pro_version\">Pro Version</string>\n    <string name=\"test_version\">Test Version</string>\n    \n    <!-- Service status -->\n    <string name=\"check_service_status\">Check Service Status</string>\n    <string name=\"algorithm_service_available\">Algorithm Service Available</string>\n    <string name=\"algorithm_service_unavailable\">Algorithm Service Unavailable</string>\n    \n    <!-- Settings -->\n    <string name=\"options_settings\">Options Settings</string>\n    \n    <!-- Validation errors -->\n    <string name=\"r_needs_int\">R needs int type</string>\n    <string name=\"g_needs_int\">G needs int type</string>\n    <string name=\"b_needs_int\">B needs int type</string>\n    <string name=\"size_needs_int\">size needs int type</string>\n    <string name=\"max_history_size_needs_int\">maxHistorySizeText needs int type</string>\n    <string name=\"please_enter_valid_url\">Please enter a valid url</string>\n    \n    <!-- User prompts -->\n    <string name=\"please_load_image_first\">Please load an image first</string>\n    \n    <!-- Filter module related -->\n    <string name=\"preview_effect\">Preview Effect</string>\n    <string name=\"previous_step\">Previous Step</string>\n    <string name=\"cancel_filter_operation\">Cancel Filter Operation</string>\n    <string name=\"save\">Save</string>\n    <string name=\"delete_original_image\">Delete Original Image</string>\n    <string name=\"remark\">Remark</string>\n    \n    <!-- API Key prompt messages -->\n    <string name=\"deepseek_api_key_missing\">Please configure DeepSeek API Key in general settings first</string>\n    <string name=\"gemini_api_key_missing\">Please configure Gemini API Key in general settings first</string>\n    <string name=\"options_settings\">Options Settings</string>\n    <string name=\"init_filter_params_config\">Initialize Filter Parameters Config</string>\n    <string name=\"clear_cache_data\">Clear Cache Data</string>\n    <string name=\"algorithm_service_url\">Algorithm Service URL</string>\n    <string name=\"enter_complete_algorithm_url\">Please enter a complete algorithm service URL address</string>\n    <string name=\"is_the_algorithm_service_available\">Is the algorithm service available</string>\n    <string name=\"algorithm_service_available\">algorithm service available</string>\n    <string name=\"algorithm_service_unavailable\">algorithm service unavailable</string>\n    <string name=\"theme_operations\">Theme Operations</string>\n    <string name=\"current_language\">Current Language</string>\n    <string name=\"chinese\">Chinese</string>\n    <string name=\"english\">English</string>\n    <string name=\"language_switch\">Language Switch</string>\n    <string name=\"language_operations\">Language Operations</string>\n    <string name=\"reset_to_chinese\">Reset to Chinese</string>\n    <string name=\"r_needs_int\">R needs int type</string>\n    <string name=\"g_needs_int\">G needs int type</string>\n    <string name=\"b_needs_int\">B needs int type</string>\n    <string name=\"size_needs_int\">size needs int type</string>\n    <string name=\"enter_valid_url\">Please enter a valid url</string>\n    <string name=\"max_history_size_needs_int\">maxHistorySizeText needs int type</string>\n    \n    <!-- Window Titles -->\n    <string name=\"window_title_doodle\">Doodle Image</string>\n    <string name=\"window_title_shape_drawing\">Shape Drawing</string>\n    <string name=\"window_title_color_pick\">Color Picker</string>\n    <string name=\"window_title_generate_gif\">Generate GIF</string>\n    <string name=\"window_title_crop_size\">Image Crop</string>\n    <string name=\"window_title_color_correction\">Color Correction</string>\n    <string name=\"window_title_filter\">Apply Filter</string>\n    <string name=\"window_title_opencv_debug\">OpenCV Debug</string>\n    <string name=\"window_title_face_swap\">Face Swap</string>\n    <string name=\"window_title_cartoon\">Image Cartoonization</string>\n    <string name=\"window_title_compression\">Image Compression</string>\n    <string name=\"window_title_web_screenshot\">Web Long Screenshot</string>\n    <string name=\"window_title_preview\">Preview</string>\n    \n    <!-- Web Screenshot -->\n    <string name=\"nodejs_not_installed\">Node.js not detected</string>\n    <string name=\"please_install_nodejs\">Please install Node.js first (https://nodejs.org/)</string>\n    <string name=\"website_url\">Website URL</string>\n    <string name=\"enter_url\">Enter URL</string>\n    <string name=\"screenshot_options\">Screenshot Options</string>\n    <string name=\"full_page_screenshot\">Full Page Screenshot</string>\n    <string name=\"wait_until\">Wait Until</string>\n    <string name=\"timeout_ms\">Timeout (ms)</string>\n    <string name=\"viewport_width\">Viewport Width</string>\n    <string name=\"viewport_height\">Viewport Height</string>\n    <string name=\"screenshot_clarity\">Screenshot Clarity Scale</string>\n    <string name=\"invalid_screenshot_clarity\">Invalid Clarity Scale</string>\n    <string name=\"please_enter_valid_screenshot_clarity\">Enter a valid scale between 0 and 4, such as 1.5 or 2.0</string>\n    <string name=\"capture_screenshot\">Capture Screenshot</string>\n    <string name=\"reset\">Reset</string>\n    <string name=\"usage_tips\">Tip: Make sure Node.js and Playwright are installed. First-time use requires running 'npx playwright install chromium' to install the browser.</string>\n    <string name=\"invalid_url\">Invalid URL</string>\n    <string name=\"please_enter_valid_url\">Please enter a valid URL (starting with http:// or https://)</string>\n</resources>\n"
  },
  {
    "path": "i18n/src/main/resources/strings/strings_zh.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<resources>\n    <!-- 主窗口和菜单 -->\n    <string name=\"app_name\">Monica</string>\n    <string name=\"app_description\">Monica 是一个跨平台图像编辑器</string>\n    <string name=\"software_version_info\">软件版本信息</string>\n    <string name=\"open_local_image\">打开本地图片</string>\n    <string name=\"load_network_image\">加载网络图片</string>\n    <string name=\"save_image\">保存图像</string>\n    <string name=\"screenshot_full_screen\">截屏（全屏）</string>\n    <string name=\"screenshot_area\">截屏（区域选择）</string>\n    <string name=\"web_screenshot\">网页长截图</string>\n    <string name=\"exit\">退出</string>\n    \n    <!-- 控制面板 -->\n    <string name=\"control_panel\">控制面板</string>\n    <string name=\"general_settings\">通用设置</string>\n    <string name=\"basic_functions\">基础功能</string>\n    <string name=\"color_correction\">图像调色</string>\n    <string name=\"filter_effects\">滤镜效果</string>\n    <string name=\"ai_laboratory\">AI 实验室</string>\n    \n    <!-- 通用设置 -->\n    <string name=\"settings\">设置</string>\n    <string name=\"monica_general_settings\">Monica 通用设置</string>\n    <string name=\"update\">更新</string>\n    <string name=\"close\">关闭</string>\n    <string name=\"cancel\">取消</string>\n    <string name=\"undo\">撤销</string>\n    <string name=\"undo_not_available\">没有可撤销的操作</string>\n    <string name=\"undo_success\">已撤销</string>\n    <string name=\"undo_and_reset_success\">已撤销并重置</string>\n    <string name=\"reset_done\">已重置</string>\n    \n    <!-- 基础功能 -->\n    <string name=\"image_blur\">图像模糊</string>\n    <string name=\"image_mosaic\">图像马赛克</string>\n    <string name=\"image_doodle\">图像涂鸦</string>\n    <string name=\"shape_drawing\">图形绘制</string>\n    <string name=\"color_picker\">颜色选择器</string>\n    <string name=\"crop_image\">图像裁剪</string>\n    <string name=\"generate_gif\">生成 GIF</string>\n    \n    <!-- 图形绘制 -->\n    <string name=\"select_color\">选择颜色</string>\n    <string name=\"change_properties\">更改属性</string>\n    <string name=\"line\">线条</string>\n    <string name=\"circle\">圆形</string>\n    <string name=\"triangle\">三角形</string>\n    <string name=\"rectangle\">矩形</string>\n    <string name=\"polygon\">多边形</string>\n    <string name=\"add_text\">添加文字</string>\n    <string name=\"save\">保存</string>\n    \n    <!-- 涂鸦功能 -->\n    <string name=\"brush\">画笔</string>\n    <string name=\"eraser\">橡皮擦</string>\n    <string name=\"clear\">清除</string>\n    <string name=\"brush_settings\">画笔设置</string>\n    <string name=\"clear_canvas\">清空画布</string>\n    <string name=\"revoke\">撤回</string>\n    \n    <!-- 颜色校正 -->\n    <string name=\"natural_language_color\">自然语言调色</string>\n    <string name=\"enter_color_instruction\">请输入调色指令</string>\n    <string name=\"color_parameters_updated\">参数已更新：%s</string>\n    <string name=\"natural_language_color_correction\">自然语言图像调色</string>\n    <string name=\"ai_provider_selection\">AI 服务提供商</string>\n    <string name=\"ai_provider_deepseek\">DeepSeek</string>\n    <string name=\"ai_provider_gemini\">Gemini</string>\n    <string name=\"contrast\">对比度</string>\n    <string name=\"hue\">色调</string>\n    <string name=\"saturation\">饱和度</string>\n    <string name=\"lightness\">亮度</string>\n    <string name=\"temperature\">色温</string>\n    <string name=\"api_key_required\">需要配置 API Key</string>\n    <string name=\"update_parameters\">更新参数：</string>\n    <string name=\"deepseek_api_key_missing\">DeepSeek API Key 未配置</string>\n    <string name=\"gemini_api_key_missing\">Gemini API Key 未配置</string>\n    <string name=\"request_failed\">请求失败：</string>\n    <string name=\"unknown_error\">未知错误</string>\n    <string name=\"highlight\">高光</string>\n    <string name=\"shadow\">阴影</string>\n    <string name=\"sharpen\">锐化</string>\n    <string name=\"corner\">边角</string>\n    <string name=\"no_significant_changes\">无明显变化</string>\n    \n    <!-- 滤镜效果 -->\n    <string name=\"filter_parameters\">滤镜参数</string>\n    \n    <!-- AI 实验室 -->\n    <string name=\"ai_laboratory_description\">AI 实验室</string>\n    <string name=\"simple_cv_algorithm\">简单 CV 算法的快速验证</string>\n    <string name=\"face_detection\">人脸检测</string>\n    <string name=\"generate_sketch\">生成素描</string>\n    <string name=\"face_swap\">人脸替换</string>\n    <string name=\"anime_style\">动漫风格</string>\n    \n    <!-- AI 实验页面 -->\n    <string name=\"home\">首页</string>\n    <string name=\"binary_image\">二值图像</string>\n    <string name=\"edge_detection\">边缘检测</string>\n    <string name=\"contour_analysis\">轮廓分析</string>\n    <string name=\"image_enhance\">图像增强</string>\n    <string name=\"image_denoising\">图像去噪</string>\n    <string name=\"morphological_operations\">形态学操作</string>\n    <string name=\"match_template\">模板匹配</string>\n    <string name=\"parameter_history\">参数历史</string>\n    \n    <!-- 裁剪功能 -->\n    <string name=\"crop_type\">裁剪类型</string>\n    <string name=\"content_scale\">内容缩放</string>\n    <string name=\"aspect_ratio\">宽高比</string>\n    <string name=\"crop_frame\">裁剪框</string>\n    <string name=\"crop_properties_settings\">裁剪属性设置</string>\n    <string name=\"confirm_crop\">确认</string>\n    <string name=\"dismiss_crop\">取消</string>\n    \n    <!-- 对话框和提示 -->\n    <string name=\"loading\">加载中...</string>\n    <string name=\"error\">错误</string>\n    <string name=\"success\">成功</string>\n    <string name=\"warning\">警告</string>\n    <string name=\"info\">信息</string>\n    \n    <!-- 属性设置 -->\n    <string name=\"alpha\">透明度</string>\n    <string name=\"font_size\">字体大小</string>\n    <string name=\"fill\">填充</string>\n    <string name=\"border\">边框</string>\n    <string name=\"stroke_width\">描边宽度</string>\n    <string name=\"stroke_cap\">描边端点</string>\n    <string name=\"stroke_join\">描边连接</string>\n    <string name=\"confirm\">确认</string>\n    \n    <!-- 版本信息 -->\n    <string name=\"pro_version\">正式版本</string>\n    <string name=\"test_version\">测试版本</string>\n    \n    <!-- 服务状态 -->\n    <string name=\"check_service_status\">检测服务状态</string>\n    <string name=\"algorithm_service_available\">算法服务可用</string>\n    <string name=\"algorithm_service_unavailable\">算法服务不可用</string>\n    \n    <!-- 设置 -->\n    <string name=\"options_settings\">选项设置</string>\n    \n    <!-- 验证错误 -->\n    <string name=\"r_needs_int\">R 需要 int 类型</string>\n    <string name=\"g_needs_int\">G 需要 int 类型</string>\n    <string name=\"b_needs_int\">B 需要 int 类型</string>\n    <string name=\"size_needs_int\">size 需要 int 类型</string>\n    <string name=\"max_history_size_needs_int\">maxHistorySizeText 需要 int 类型</string>\n    <string name=\"please_enter_valid_url\">请输入一个正确的 url</string>\n    \n    <!-- 用户提示 -->\n    <string name=\"please_load_image_first\">请先加载图片</string>\n    \n    <!-- 滤镜模块相关 -->\n    <string name=\"preview_effect\">预览效果</string>\n    <string name=\"previous_step\">上一步</string>\n    <string name=\"cancel_filter_operation\">取消滤镜操作</string>\n    <string name=\"save\">保存</string>\n    <string name=\"delete_original_image\">删除原图</string>\n    <string name=\"remark\">备注</string>\n    \n    <!-- 语言设置 -->\n    <string name=\"language_settings\">语言设置</string>\n    <string name=\"current_language\">当前语言</string>\n    \n    <!-- 网络图片加载 -->\n    <string name=\"load_network_image_dialog\">加载网络图片</string>\n\n    <!-- 主题设置 -->\n    <string name=\"basic_settings\">基础设置</string>\n    <string name=\"api_settings\">API设置</string>\n    <string name=\"theme_settings\">主题设置</string>\n    <string name=\"language_settings\">语言设置</string>\n    <string name=\"current_theme\">当前主题</string>\n    <string name=\"select_theme\">选择主题</string>\n    <string name=\"theme_light\">浅色主题</string>\n    <string name=\"theme_dark\">深色主题</string>\n    <string name=\"theme_blue\">蓝色主题</string>\n    <string name=\"theme_green\">绿色主题</string>\n    <string name=\"theme_purple\">紫色主题</string>\n    <string name=\"theme_orange\">橙色主题</string>\n    <string name=\"theme_pink\">粉色主题</string>\n    <string name=\"reset_to_default_theme\">重置为默认主题</string>\n    <string name=\"enter_image_url\">请输入图片URL</string>\n    <string name=\"load\">加载</string>\n    <string name=\"url_invalid\">无效的URL格式</string>\n    \n    <!-- 版本信息 -->\n    <string name=\"version_info\">版本信息</string>\n    <string name=\"copyright\">© 2024 Tony Shen. 保留所有权利。</string>\n    \n    <!-- 状态和消息 -->\n    <string name=\"basic_function_cancelled\">基础功能已取消</string>\n    <string name=\"basic_function_selected\">基础功能已选择</string>\n    <string name=\"general_settings_cancelled\">通用设置已取消</string>\n    <string name=\"general_settings_selected\">通用设置已选择</string>\n    <string name=\"color_correction_cancelled\">图像调色已取消</string>\n    <string name=\"color_correction_selected\">图像调色已选择</string>\n    <string name=\"filter_cancelled\">滤镜效果已取消</string>\n    <string name=\"filter_selected\">滤镜效果已选择</string>\n    <string name=\"ai_laboratory_cancelled\">AI 实验室已取消</string>\n    <string name=\"ai_laboratory_selected\">AI 实验室已选择</string>\n    \n    <!-- 工具提示 -->\n    <string name=\"simple_cv_tooltip\">简单 CV 算法的快速验证</string>\n    <string name=\"face_detect_tooltip\">人脸检测</string>\n    <string name=\"sketch_drawing_tooltip\">生成素描</string>\n    <string name=\"face_swap_tooltip\">人脸替换</string>\n    <string name=\"anime_style_tooltip\">动漫风格</string>\n    \n    <!-- 版本信息 -->\n    <string name=\"monica_software_info\">Monica 软件信息</string>\n    <string name=\"monica_version_info\">Monica 版本：%s，%s，构建时间：%s</string>\n    <string name=\"opencv_version_info\">OpenCV 版本：%s，本地算法库：%s</string>\n    <string name=\"copyright_info\">版权：版权所有 2024-至今，Tony Shen</string>\n    <string name=\"github_url\">Github 地址：https://github.com/fengzhizi715/Monica</string>\n    \n    <!-- 设置相关 -->\n    <string name=\"output_box_color_settings\">输出框颜色设置：</string>\n    <string name=\"area_size_settings\">区域大小设置（用于模糊/马赛克）：</string>\n    <string name=\"max_history_size\">每个模块的最大历史记录大小：</string>\n    <string name=\"algorithm_service_url\">算法服务地址：</string>\n    <string name=\"init_filter_params\">初始化滤镜参数配置</string>\n    <string name=\"clear_cache_data\">清除所有缓存数据</string>\n    <string name=\"reset_to_system_language\">重置</string>\n    \n    <!-- Basic functions -->\n    <string name=\"x_direction\">X 方向</string>\n    <string name=\"y_direction\">Y 方向</string>\n    \n    <!-- 过滤 effects -->\n    <string name=\"enter_filter\">进入滤镜界面</string>\n    <string name=\"select_filter\">请选择滤镜</string>\n    <string name=\"filter_name\">%s 滤镜</string>\n    <string name=\"select_filter_first\">请先选择滤镜</string>\n    <string name=\"image_editor_filter_module\">图像编辑器：滤镜模块</string>\n    <string name=\"export\">导出</string>\n    <string name=\"adjustments\">调整</string>\n    <string name=\"reset_filter\">重置滤镜</string>\n    <string name=\"apply\">应用</string>\n    <string name=\"failed_to_apply_filter\">应用滤镜失败</string>\n    <string name=\"filter_applied\">滤镜已应用</string>\n    <string name=\"no_image\">暂无图片</string>\n    <string name=\"no_filters_found\">未找到匹配的滤镜</string>\n    <string name=\"param_summary\">参数摘要</string>\n    <string name=\"param_summary_default\">当前为默认参数</string>\n    <string name=\"param_summary_changed_count\">已调整 %d 项</string>\n    <string name=\"param_summary_reset_hint\">提示：可在底部点击“重置滤镜”恢复默认参数</string>\n    <string name=\"clear_filter\">清除滤镜</string>\n    <!-- ColorFilter style options -->\n    <string name=\"color_filter_style_0\">秋色</string>\n    <string name=\"color_filter_style_1\">骨骼</string>\n    <string name=\"color_filter_style_2\">冷色</string>\n    <string name=\"color_filter_style_3\">暖色</string>\n    <string name=\"color_filter_style_4\">HSV</string>\n    <string name=\"color_filter_style_5\">喷射</string>\n    <string name=\"color_filter_style_6\">海洋</string>\n    <string name=\"color_filter_style_7\">粉色</string>\n    <string name=\"color_filter_style_8\">彩虹</string>\n    <string name=\"color_filter_style_9\">春天</string>\n    <string name=\"color_filter_style_10\">夏天</string>\n    <string name=\"color_filter_style_11\">冬天</string>\n    <!-- NatureFilter style options -->\n    <string name=\"nature_filter_style_1\">大气</string>\n    <string name=\"nature_filter_style_2\">燃烧</string>\n    <string name=\"nature_filter_style_3\">雾霾</string>\n    <string name=\"nature_filter_style_4\">冰冻</string>\n    <string name=\"nature_filter_style_5\">熔岩</string>\n    <string name=\"nature_filter_style_6\">金属</string>\n    <string name=\"nature_filter_style_7\">海洋</string>\n    <string name=\"nature_filter_style_8\">水流</string>\n    <string name=\"notes\">备注：</string>\n    <string name=\"search\">搜索</string>\n    <string name=\"fit\">适应</string>\n    \n    <!-- Anime style -->\n    <string name=\"select_anime_style\">请选择动漫风格</string>\n    \n    <!-- GIF generation -->\n    <string name=\"add_image_first\">请先添加图片</string>\n    <string name=\"select_images\">选择图片</string>\n    <string name=\"gif_generation_strategy\">GIF 生成策略</string>\n    <string name=\"gif_width\">GIF 宽度</string>\n    <string name=\"gif_height\">GIF 高度</string>\n    <string name=\"frame_interval\">帧间隔（毫秒）</string>\n    <string name=\"loop_playback\">循环播放</string>\n    \n    <!-- AI experiment related -->\n    <string name=\"gaussian_filter\">高斯滤波</string>\n    <string name=\"median_filter\">中值滤波</string>\n    <string name=\"gaussian_bilateral_filter\">高斯双边滤波</string>\n    <string name=\"mean_shift_filter\">均值漂移滤波</string>\n    <string name=\"grayscale_image\">灰度图像</string>\n    <string name=\"threshold_segmentation\">阈值分割</string>\n    <string name=\"canny_edge_detection\">Canny 边缘检测</string>\n    <string name=\"color_image_segmentation\">彩色图像分割</string>\n    <string name=\"template\">模板</string>\n    <string name=\"matching_method\">匹配方式</string>\n    <string name=\"rotation\">旋转</string>\n    <string name=\"min_angle\">最小角度</string>\n    <string name=\"max_angle\">最大角度</string>\n    <string name=\"angle_step\">角度步长</string>\n    <string name=\"scale\">缩放</string>\n    <string name=\"min_scale\">最小缩放</string>\n    <string name=\"max_scale\">最大缩放</string>\n    <string name=\"scale_step\">缩放步长</string>\n    <string name=\"template_matching_params\">模板匹配参数</string>\n    <string name=\"threshold\">阈值</string>\n    <string name=\"nms_params\">NMS 参数</string>\n    <string name=\"score_threshold\">分数阈值</string>\n    <string name=\"nms_threshold\">NMS 阈值</string>\n    <string name=\"filter_settings\">过滤设置</string>\n    <string name=\"min_perimeter\">最小值</string>\n    <string name=\"max_perimeter\">最大值</string>\n    <string name=\"min_area\">最小值</string>\n    <string name=\"max_area\">最大值</string>\n    <string name=\"min_roundness\">最小值</string>\n    <string name=\"max_roundness\">最大值</string>\n    <string name=\"min_aspect_ratio\">最小值</string>\n    <string name=\"max_aspect_ratio\">最大值</string>\n    <string name=\"display_settings\">显示 设置</string>\n    <string name=\"operation_element\">操作元素</string>\n    <string name=\"structural_element\">结构元素</string>\n    <string name=\"width\">宽度</string>\n    <string name=\"height\">高度</string>\n    <string name=\"histogram_equalization\">直方图均衡化</string>\n    <string name=\"clahe\">对比度 受限自适应 直方图均衡化 (CLAHE)</string>\n    <string name=\"gamma_transform\">伽马变换</string>\n    <string name=\"laplace_sharpening\">拉普拉斯锐化</string>\n    <string name=\"usm_sharpening\">USM 锐化</string>\n    <string name=\"automatic_color_balance\">自动色彩平衡</string>\n    <string name=\"edge_detection_operator\">边缘检测 算子</string>\n    \n    <!-- Face swap -->\n    <string name=\"replace_target_face_count\">替换目标中的人脸数量</string>\n    \n    <!-- Common prompts -->\n    <string name=\"click_to_select_image\">点击选择图片</string>\n    \n    <!-- Window titles -->\n    <string name=\"monica_image_editor\">Monica 图像编辑器</string>\n    <string name=\"notification\">通知</string>\n    <string name=\"export_image\">导出图片</string>\n    \n    <!-- Common labels -->\n    <string name=\"min_value\">最小值</string>\n    <string name=\"max_value\">最大值</string>\n    \n    <!-- Buttons and operations -->\n    <string name=\"restore\">恢复</string>\n    <string name=\"restore_original\">恢复 最初</string>\n    <string name=\"previous_step\">上一步</string>\n    <string name=\"enlarge_preview\">放大预览</string>\n    <string name=\"delete\">删除</string>\n    \n    <!-- 图像 operations -->\n    <string name=\"image_color_correction\">图像调色</string>\n    <string name=\"enter_image_color_correction\">进入图像调色界面</string>\n    <string name=\"image_flip\">图像翻转</string>\n    <string name=\"image_rotate\">图像旋转</string>\n    <string name=\"image_scale\">图像 缩放</string>\n    <string name=\"image_shear\">图像错切</string>\n    <string name=\"image_crop\">图像裁剪</string>\n    <string name=\"image_compression\">图像压缩</string>\n    <string name=\"original\">原始</string>\n    <string name=\"compressed\">压缩后</string>\n    <string name=\"input_selection\">输入选择</string>\n    <string name=\"single_image\">单张图片</string>\n    <string name=\"batch_folder\">文件夹(批量)</string>\n    \n    <!-- 图像压缩相关 -->\n    <string name=\"compression_mode\">压缩模式</string>\n    <string name=\"compression_algorithm\">压缩算法</string>\n    <string name=\"quality_setting\">质量设置（视觉无损）</string>\n    <string name=\"quality_description\">值越高，压缩率越低，文件越大；值越低，压缩率越高，但可能失去细节</string>\n    <string name=\"compression_level\">压缩级别</string>\n    <string name=\"compression_level_description\">级别越高，压缩率越高，但处理时间越长；级别越低，处理速度越快</string>\n    <string name=\"compress_image\">压缩图片</string>\n    <string name=\"select_input_folder\">选择输入文件夹</string>\n    <string name=\"select_output_folder\">选择输出文件夹</string>\n    <string name=\"start_batch_compression\">开始批量压缩</string>\n    <string name=\"compression_result\">压缩结果</string>\n    <string name=\"original_size\">原始大小</string>\n    <string name=\"compressed_size\">压缩后大小</string>\n    <string name=\"compression_ratio\">压缩率</string>\n    <string name=\"size_increase\">变大</string>\n    <string name=\"compressed_file_larger_warning\">提示：压缩后文件反而更大。建议降低质量或更换算法（例如 JPG 用 JPEG 质量压缩；截图/透明图用 PNG 优化）。</string>\n    <string name=\"selected\">已选择</string>\n    <string name=\"error_please_select_image\">错误：请先选择图片</string>\n    <string name=\"compressing_image\">正在压缩图片...</string>\n    <string name=\"webp_not_supported\">当前系统不支持 WebP 格式，已自动转换为 %s</string>\n    <string name=\"webp_encode_failed\">WebP 编码失败，已自动转换为 %s</string>\n    <string name=\"compression_success\">压缩成功！</string>\n    <string name=\"compression_failed\">压缩失败</string>\n    <string name=\"compression_error\">压缩异常: %s</string>\n    <string name=\"preparing_batch_compression\">准备批量压缩...</string>\n    <string name=\"no_images_in_folder\">文件夹中没有图片</string>\n    <string name=\"compression_cancelled\">压缩已取消</string>\n    <string name=\"compressing_file\">正在压缩: %s (%d/%d)</string>\n    <string name=\"cannot_read_image\">无法读取图片</string>\n    <string name=\"batch_compression_completed\">批量压缩完成（成功: %d/%d）</string>\n    <string name=\"batch_compression_error\">批量压缩异常: %s</string>\n    <string name=\"please_select_image_to_compress\">请选择要压缩的图片</string>\n    <string name=\"select_image\">选择图片</string>\n    <string name=\"please_select_or_load_image\">请先选择或加载图片</string>\n    <string name=\"file_overwrite_confirm\">文件覆盖确认</string>\n    <string name=\"file_exists_overwrite\">文件 \\\"%s\\\" 已存在，是否覆盖？</string>\n    <string name=\"save_compressed_image\">保存压缩后的图片</string>\n    <string name=\"please_compress_first\">请先执行压缩</string>\n    <string name=\"start_compression\">开始压缩</string>\n    <string name=\"please_select_image_in_preview\">请先在右侧预览区选择图片</string>\n    <string name=\"batch_mode_no_preview\">批量模式不支持预览对比，请查看进度与统计结果</string>\n    <string name=\"reset\">重置</string>\n    <string name=\"applied_to_editor\">已应用到编辑器</string>\n    <string name=\"apply_to_editor\">应用到编辑器</string>\n    <string name=\"save_success\">保存成功：%s</string>\n    <string name=\"save_failed\">保存失败</string>\n    <string name=\"save\">保存</string>\n    <string name=\"cannot_read_image_file\">无法读取图片文件</string>\n    <string name=\"load_image_failed\">加载图片失败: %s</string>\n    <string name=\"image_cleared\">已清除图像</string>\n    <string name=\"clear_image\">清除图像</string>\n    <string name=\"webp_not_supported_auto_convert\">当前系统不支持 WebP，将自动转换为 %s</string>\n    <string name=\"batch_compression_warning\">批量压缩警告</string>\n    <string name=\"output_folder_has_files\">输出文件夹中已有 %d 个文件，批量压缩可能会覆盖同名文件。是否继续？</string>\n    <string name=\"cancel\">取消</string>\n    <string name=\"format_conversion_warning_jpg_to_png\">注意：将 JPG 转换为 PNG 可能导致文件变大，因为 PNG 是无损格式</string>\n    <string name=\"format_conversion_warning_jpg_to_webp_lossless\">注意：将 JPG 转换为 WebP Lossless 可能导致文件变大</string>\n    \n    <!-- Anime styles -->\n    <string name=\"miyazaki_style\">宫崎骏风格</string>\n    <string name=\"japanese_portrait_style\">日系人像风格</string>\n    <string name=\"black_white_line_art\">Black &amp; White 线条 艺术</string>\n    <string name=\"shinkai_style\">新海诚风格</string>\n    <string name=\"cute_style\">可爱风格</string>\n    \n    <!-- 算法 service -->\n    <string name=\"algorithm_service_error\">算法 服务 错误</string>\n    \n    <!-- 图像 processing algorithms -->\n    <string name=\"image_grayscale\">图像灰度化</string>\n    <string name=\"threshold_type\">阈值 类型</string>\n    <string name=\"global_threshold_segmentation\">全局 阈值分割</string>\n    <string name=\"adaptive_threshold_segmentation\">自适应 阈值分割</string>\n    <string name=\"adaptive_threshold_algorithm\">自适应 阈值 算法</string>\n    \n    <!-- Edge detection operators -->\n    <string name=\"roberts_operator\">Roberts 算子</string>\n    <string name=\"prewitt_operator\">Prewitt 算子</string>\n    <string name=\"sobel_operator\">Sobel 算子</string>\n    <string name=\"laplace_operator\">Laplace 算子</string>\n    <string name=\"log_operator\">LoG 算子</string>\n    <string name=\"dog_operator\">DoG 算子</string>\n    <string name=\"first_derivative_operator\">一阶导数算子</string>\n    <string name=\"second_derivative_operator\">二阶导数算子</string>\n    <string name=\"first_derivative_edge_detection\">一阶导数边缘检测</string>\n    <string name=\"second_derivative_edge_detection\">二阶导数边缘检测</string>\n    <string name=\"canny_operator\">Canny 算子</string>\n    <string name=\"canny_edge_detection_full\">Canny 边缘检测</string>\n    \n    <!-- Prompt messages -->\n    <string name=\"click_to_select_image_or_drag\">请点击选择图像或拖拽图像至此</string>\n    <string name=\"image_save_success\">图像保存成功</string>\n    <string name=\"image_save_failed\">图像保存失败</string>\n    <string name=\"please_select_first_derivative_operator\">请选择一阶导数算子类型</string>\n    <string name=\"please_select_second_derivative_operator\">请选择二阶导数算子类型</string>\n    <string name=\"please_select_threshold_type\">请选择阈值化类型</string>\n    <string name=\"please_select_global_threshold_segmentation\">请选择全局阈值分割类型</string>\n    <string name=\"please_select_adaptive_threshold_algorithm\">请选择自适应阈值算法类型</string>\n    <string name=\"please_select_threshold_type_and_segmentation\">请选择阈值化类型 and global threshold segmentation or adaptive threshold segmentation</string>\n    \n    <!-- Parameter validation errors -->\n    <string name=\"ksize_needs_int\">ksize 需要 int 类型</string>\n    <string name=\"sigma_x_needs_double\">sigmaX 需要 double 类型</string>\n    <string name=\"sigma_y_needs_double\">sigmaY 需要 double 类型</string>\n    <string name=\"d_needs_int\">d 需要 int 类型</string>\n    <string name=\"sigma_color_needs_double\">sigmaColor 需要 double 类型</string>\n    <string name=\"sigma_space_needs_double\">sigmaSpace 需要 double 类型</string>\n    <string name=\"sp_needs_double\">sp 需要 double 类型</string>\n    <string name=\"sr_needs_double\">sr 需要 double 类型</string>\n    <string name=\"width_needs_int\">width 需要 int 类型</string>\n    <string name=\"height_needs_int\">height 需要 int 类型</string>\n    <string name=\"x_direction_needs_float\">x 方向 需要 float 类型</string>\n    <string name=\"y_direction_needs_float\">y 方向 需要 float 类型</string>\n    <string name=\"block_size_needs_int\">blockSize 需要 int 类型</string>\n    <string name=\"c_needs_int\">c 需要 int 类型</string>\n    <string name=\"threshold1_needs_double\">threshold1 需要 double 类型</string>\n    <string name=\"threshold2_needs_double\">threshold2 需要 double 类型</string>\n    <string name=\"aperture_size_needs_int\">apertureSize 需要 int 类型</string>\n    <string name=\"hmin_needs_int\">hmin 需要 int 类型</string>\n    <string name=\"smin_needs_int\">smin 需要 int 类型</string>\n    <string name=\"vmin_needs_int\">vmin 需要 int 类型</string>\n    <string name=\"hmax_needs_int\">hmax 需要 int 类型</string>\n    <string name=\"smax_needs_int\">smax 需要 int 类型</string>\n    <string name=\"vmax_needs_int\">vmax 需要 int 类型</string>\n    \n    <!-- Additional parameter validation errors -->\n    <string name=\"sigma1_needs_double\">sigma1 需要 double 类型</string>\n    <string name=\"sigma2_needs_double\">sigma2 需要 double 类型</string>\n    <string name=\"size_needs_int\">size 需要 int 类型</string>\n    \n    <!-- AI 实验页面相关 -->\n    <string name=\"experiment_home_description\">本模块的算法使用 OpenCV C++ 实现，目前只适用于一些简单 CV 算法的快速验证和调参。</string>\n    \n    <!-- 实验页面导航 -->\n    <string name=\"experiment_home\">首页</string>\n    <string name=\"experiment_binary_image\">二值化</string>\n    <string name=\"experiment_edge_detection\">边缘检测</string>\n    <string name=\"experiment_contour_analysis\">轮廓分析</string>\n    <string name=\"experiment_image_enhance\">图像增强</string>\n    <string name=\"experiment_image_denoising\">图像降噪</string>\n    <string name=\"experiment_morphological_operations\">形态学操作</string>\n    <string name=\"experiment_match_template\">模版匹配</string>\n    <string name=\"experiment_history\">调参历史</string>\n    \n    <!-- 实验页面通用操作 -->\n    <string name=\"please_select_image_first\">请先选择图像</string>\n    \n    <!-- 二值化页面 -->\n    <string name=\"please_binarize_image_first\">请先将当前图像进行二值化</string>\n    \n    <!-- 轮廓分析页面 -->\n    <string name=\"contour_filter_settings\">轮廓过滤设置</string>\n    <string name=\"contour_display_settings\">轮廓显示设置</string>\n    <string name=\"perimeter\">周长</string>\n    <string name=\"area\">面积</string>\n    <string name=\"roundness\">圆度</string>\n    <string name=\"show_original_image\">原图显示</string>\n    <string name=\"show_bounding_rect\">外接矩形</string>\n    <string name=\"show_min_area_rect\">最小外接矩形</string>\n    <string name=\"show_center\">质心</string>\n    <string name=\"perimeter_min_needs_double\">周长最小值需要 double 类型</string>\n    <string name=\"perimeter_max_needs_double\">周长最大值需要 double 类型</string>\n    <string name=\"area_min_needs_double\">面积最小值需要 double 类型</string>\n    <string name=\"area_max_needs_double\">面积最大值需要 double 类型</string>\n    <string name=\"roundness_min_needs_double\">圆度最小值需要 double 类型</string>\n    <string name=\"roundness_max_needs_double\">圆度最大值需要 double 类型</string>\n    <string name=\"aspect_ratio_min_needs_double\">长宽比最小值需要 double 类型</string>\n    <string name=\"aspect_ratio_max_needs_double\">长宽比最大值需要 double 类型</string>\n    <string name=\"perimeter_at_least_one_value\">周长至少输入一个最小值或最大值</string>\n    <string name=\"area_at_least_one_value\">面积至少输入一个最小值或最大值</string>\n    <string name=\"roundness_at_least_one_value\">圆度至少输入一个最小值或最大值</string>\n    <string name=\"aspect_ratio_at_least_one_value\">长宽比至少输入一个最小值或最大值</string>\n    <string name=\"please_binarize_image_first_for_contour\">请先将当前图像进行二值化</string>\n    \n    <!-- 图像增强页面 -->\n    <string name=\"laplace_sharpen\">Laplace 锐化</string>\n    <string name=\"usm_sharpen\">USM 锐化</string>\n    <string name=\"auto_color_balance\">自动色彩均衡</string>\n    <string name=\"clip_limit_needs_double\">clipLimit 需要 double 类型</string>\n    <string name=\"size_needs_int_for_enhance\">size 需要 int 类型</string>\n    <string name=\"gamma_needs_float\">gamma 需要 float 类型</string>\n    <string name=\"radius_needs_int\">Radius 需要 int 类型</string>\n    <string name=\"threshold_needs_int\">Threshold 需要 int 类型</string>\n    <string name=\"amount_needs_int\">Amount 需要 int 类型</string>\n    <string name=\"ratio_needs_int\">Ratio 需要 int 类型</string>\n    \n    <!-- 形态学操作页面 -->\n    <string name=\"operating_elements\">操作元素</string>\n    <string name=\"structural_elements\">结构元素</string>\n    <string name=\"erosion\">腐蚀</string>\n    <string name=\"dilation\">膨胀</string>\n    <string name=\"opening\">开操作</string>\n    <string name=\"closing\">闭操作</string>\n    <string name=\"morphological_gradient\">形态学梯度</string>\n    <string name=\"top_hat\">顶帽</string>\n    <string name=\"black_hat\">黑帽</string>\n    <string name=\"hit_miss\">击中击不中</string>\n    <string name=\"cross\">十字交叉</string>\n    <string name=\"ellipse\">椭圆形</string>\n    <string name=\"width_needs_int_for_morph\">width 需要 int 类型</string>\n    <string name=\"height_needs_int_for_morph\">height 需要 int 类型</string>\n    \n    <!-- 模版匹配页面 -->\n    <string name=\"original_image_matching\">原图匹配</string>\n    <string name=\"grayscale_matching\">灰度匹配</string>\n    <string name=\"edge_matching\">边缘匹配</string>\n    <string name=\"import_template\">导入模版：</string>\n    <string name=\"delete_source_image\">删除 source 的图</string>\n    <string name=\"please_import_template_first\">请先导入模版文件</string>\n    <string name=\"angle_start_needs_int\">angleStart 需要 int 类型， 且 angleStart &gt;= 0</string>\n    <string name=\"angle_end_needs_int\">angleEnd 需要 int 类型， 且 angleEnd &lt;= 360</string>\n    <string name=\"angle_step_needs_int\">angleStep 需要 int 类型， 且 angleStep &gt; 0</string>\n    <string name=\"scale_start_needs_double\">scaleStart 需要 double 类型， 且 scaleStart &gt;= 0</string>\n    <string name=\"scale_end_needs_double\">scaleEnd 需要 double 类型， 且 scaleStart &lt;= 1.0</string>\n    <string name=\"scale_step_needs_double\">scaleStep 需要 double 类型， 且 scaleStep &gt; 0</string>\n    <string name=\"match_template_threshold_needs_double\">matchTemplateThreshold 需要 double 类型， 且 matchTemplateThreshold &gt;= 0</string>\n    <string name=\"score_threshold_needs_float\">scoreThreshold 需要 float 类型， 且 scoreThreshold &gt;= 0</string>\n    <string name=\"nms_threshold_needs_float\">nmsThreshold 需要 float 类型， 且 nmsThreshold &gt;= 0</string>\n    <string name=\"template_matching\">模版匹配</string>\n    \n    <!-- 历史记录页面 -->\n    <string name=\"operation\">操作</string>\n    <string name=\"time\">时间</string>\n    <string name=\"parameters\">参数</string>\n    <string name=\"description\">描述</string>\n    \n    <!-- 日志信息 -->\n    <string name=\"threshold_type_cancelled\">取消了阈值化类型</string>\n    <string name=\"threshold_type_selected\">勾选了阈值化类型</string>\n    <string name=\"global_threshold_cancelled\">取消了全局阈值分割</string>\n    <string name=\"global_threshold_selected\">勾选了全局阈值分割</string>\n    <string name=\"adaptive_threshold_cancelled\">取消了自适应阈值分割</string>\n    <string name=\"adaptive_threshold_selected\">勾选了自适应阈值分割</string>\n    <string name=\"opencv_debug_view_init\">OpenCVDebugView 启动时初始化</string>\n    <string name=\"opencv_debug_view_dispose\">OpenCVDebugView 关闭时释放资源</string>\n    \n    <!-- 图像增强页面按钮文本 -->\n    <string name=\"histogram_equalization_button\">直方图均衡化</string>\n    <string name=\"clahe_button\">CLAHE</string>\n    <string name=\"gamma_transform_button\">伽马变换</string>\n    <string name=\"laplace_sharpen_button\">Laplace 锐化</string>\n    <string name=\"usm_sharpen_button\">USM 锐化</string>\n    <string name=\"auto_color_balance_button\">自动色彩均衡</string>\n    \n    <!-- 缺失的翻译 -->\n    <string name=\"please_select_canny_operator\">请选择 Canny 算子</string>\n    \n    <!-- Gemini API 设置 -->\n    <string name=\"gemini_api_key\">Gemini API Key</string>\n    <string name=\"gemini_api_key_title\">Gemini API 密钥</string>\n    <string name=\"gemini_api_key_description\">用于自然语言调色的 Gemini API 密钥</string>\n    <string name=\"options_settings\">选项设置</string>\n    <string name=\"init_filter_params_config\">初始化滤镜参数配置</string>\n    <string name=\"clear_cache_data\">清除缓存数据</string>\n    <string name=\"algorithm_service_url\">算法服务URL</string>\n    <string name=\"enter_complete_algorithm_url\">请输入完整的算法服务URL地址</string>\n    <string name=\"is_the_algorithm_service_available\">算法服务是否可用</string>\n    <string name=\"algorithm_service_available\">算法服务可用</string>\n    <string name=\"algorithm_service_unavailable\">算法服务不可用</string>\n    <string name=\"theme_operations\">主题操作</string>\n    <string name=\"current_language\">当前语言</string>\n    <string name=\"chinese\">中文</string>\n    <string name=\"english\">English</string>\n    <string name=\"language_switch\">语言切换</string>\n    <string name=\"language_operations\">语言操作</string>\n    <string name=\"reset_to_chinese\">重置为中文</string>\n    <string name=\"r_needs_int\">R 需要 int 类型</string>\n    <string name=\"g_needs_int\">G 需要 int 类型</string>\n    <string name=\"b_needs_int\">B 需要 int 类型</string>\n    <string name=\"size_needs_int\">size 需要 int 类型</string>\n    <string name=\"enter_valid_url\">请输入一个正确的 url</string>\n    <string name=\"max_history_size_needs_int\">maxHistorySizeText 需要 int 类型</string>\n    \n    <!-- 窗口标题 -->\n    <string name=\"window_title_doodle\">涂鸦图像</string>\n    <string name=\"window_title_shape_drawing\">形状绘制</string>\n    <string name=\"window_title_color_pick\">图像取色</string>\n    <string name=\"window_title_generate_gif\">生成 gif</string>\n    <string name=\"window_title_crop_size\">图像裁剪</string>\n    <string name=\"window_title_color_correction\">图像调色</string>\n    <string name=\"window_title_filter\">使用滤镜</string>\n    <string name=\"window_title_opencv_debug\">简单 CV 算法的快速验证</string>\n    <string name=\"window_title_face_swap\">人脸替换</string>\n    <string name=\"window_title_cartoon\">图像动漫化</string>\n    <string name=\"window_title_compression\">图像压缩</string>\n    <string name=\"window_title_web_screenshot\">网页长截图</string>\n    <string name=\"window_title_preview\">放大预览</string>\n    \n    <!-- 网页截图 -->\n    <string name=\"nodejs_not_installed\">未检测到 Node.js 环境</string>\n    <string name=\"please_install_nodejs\">请先安装 Node.js (https://nodejs.org/)</string>\n    <string name=\"website_url\">网站地址</string>\n    <string name=\"enter_url\">请输入网页URL</string>\n    <string name=\"screenshot_options\">截图选项</string>\n    <string name=\"full_page_screenshot\">全页截图</string>\n    <string name=\"wait_until\">等待策略</string>\n    <string name=\"timeout_ms\">超时时间(毫秒)</string>\n    <string name=\"viewport_width\">视口宽度</string>\n    <string name=\"viewport_height\">视口高度</string>\n    <string name=\"screenshot_clarity\">截图清晰度倍率</string>\n    <string name=\"invalid_screenshot_clarity\">清晰度倍率无效</string>\n    <string name=\"please_enter_valid_screenshot_clarity\">请输入 0 到 4 之间的有效倍率，例如 1.5 或 2.0</string>\n    <string name=\"capture_screenshot\">开始截图</string>\n    <string name=\"reset\">重置</string>\n    <string name=\"usage_tips\">提示：确保已安装 Node.js 和 Playwright。首次使用需要运行 'npx playwright install chromium' 安装浏览器。</string>\n    <string name=\"invalid_url\">无效的URL</string>\n    <string name=\"please_enter_valid_url\">请输入有效的URL（以 http:// 或 https:// 开头）</string>\n</resources>\n"
  },
  {
    "path": "i18n/src/test/kotlin/cn/netdiscovery/monica/i18n/InternationalizationTest.kt",
    "content": "package cn.netdiscovery.monica.i18n\n\nimport org.junit.Test\nimport org.junit.Assert.*\nimport org.junit.Before\n\n/**\n * 国际化功能测试\n */\nclass InternationalizationTest {\n\n    @Test\n    fun `test language enum`() {\n        assertEquals(\"zh\", Language.CHINESE.code)\n        assertEquals(\"en\", Language.ENGLISH.code)\n        assertEquals(\"中文\", Language.CHINESE.displayName)\n        assertEquals(\"English\", Language.ENGLISH.displayName)\n        assertEquals(\"🇨🇳\", Language.CHINESE.flag)\n        assertEquals(\"🇺🇸\", Language.ENGLISH.flag)\n    }\n\n    @Test\n    fun `test language fromCode`() {\n        assertEquals(Language.CHINESE, Language.fromCode(\"zh\"))\n        assertEquals(Language.ENGLISH, Language.fromCode(\"en\"))\n        assertEquals(Language.CHINESE, Language.fromCode(\"invalid\")) // 默认返回中文\n    }\n\n    @Test\n    fun `test system language detection`() {\n        val systemLang = Language.getSystemLanguage()\n        assertTrue(systemLang in Language.values())\n    }\n\n    @Test\n    fun `test localization manager`() {\n        // 测试默认语言\n        val defaultLang = LocalizationManager.currentLanguage\n        assertTrue(defaultLang in Language.values())\n        \n        // 测试语言切换\n        val originalLang = LocalizationManager.currentLanguage\n        val newLang = if (originalLang == Language.CHINESE) Language.ENGLISH else Language.CHINESE\n        \n        LocalizationManager.setLanguage(newLang)\n        assertEquals(newLang, LocalizationManager.currentLanguage)\n        \n        // 恢复原语言\n        LocalizationManager.setLanguage(originalLang)\n    }\n\n    @Test\n    fun `test string resource loading`() {\n        val chineseResource = LocalizationManager.getXmlResource(Language.CHINESE)\n        val englishResource = LocalizationManager.getXmlResource(Language.ENGLISH)\n        \n        // 测试资源是否加载成功\n        assertNotNull(chineseResource)\n        assertNotNull(englishResource)\n        \n        // 测试获取字符串\n        val chineseAppName = chineseResource.get(\"app_name\")\n        val englishAppName = englishResource.get(\"app_name\")\n        \n        assertEquals(\"Monica\", chineseAppName)\n        assertEquals(\"Monica\", englishAppName)\n    }\n\n    @Test\n    fun `test string resource with parameters`() {\n        val chineseResource = LocalizationManager.getXmlResource(Language.CHINESE)\n        val englishResource = LocalizationManager.getXmlResource(Language.ENGLISH)\n        \n        // 测试带参数的字符串\n        val chineseParam = chineseResource.get(\"color_parameters_updated\", \"亮度+10\")\n        val englishParam = englishResource.get(\"color_parameters_updated\", \"brightness+10\")\n        \n        assertTrue(chineseParam.contains(\"亮度+10\"))\n        assertTrue(englishParam.contains(\"brightness+10\"))\n    }\n\n    @Test\n    fun `test supported languages`() {\n        val supportedLanguages = LocalizationManager.getSupportedLanguages()\n        assertEquals(2, supportedLanguages.size)\n        assertTrue(supportedLanguages.contains(Language.CHINESE))\n        assertTrue(supportedLanguages.contains(Language.ENGLISH))\n    }\n}\n"
  },
  {
    "path": "i18n/string_manager.sh",
    "content": "#!/bin/bash\n\n# 字符串资源文件综合管理工具\n# 功能：检查重复项、清理重复项、比对中英文文件\n# 作者: AI Assistant\n# 版本: 2.0\n\n# 颜色定义\nRED='\\033[0;31m'\nGREEN='\\033[0;32m'\nYELLOW='\\033[1;33m'\nBLUE='\\033[0;34m'\nNC='\\033[0m' # No Color\n\n# 默认参数\nVERBOSE=false\nDRY_RUN=false\nBACKUP=true\nAUTO_FIX=false\nCOMPARE_MODE=false\nCLEANUP_MODE=false\nPOSITION_MODE=false\n\n# 显示帮助信息\nshow_help() {\n    echo \"字符串资源文件综合管理工具 v2.0\"\n    echo \"\"\n    echo \"用法: $0 [选项] <文件路径>\"\n    echo \"\"\n    echo \"模式选项:\"\n    echo \"  -c, --cleanup      清理模式：检查并清理重复项\"\n    echo \"  -m, --compare      比对模式：比对中英文文件差异\"\n    echo \"  -p, --position     位置比对模式：检查相同key的行号是否一致\"\n    echo \"  -a, --all          全功能模式：清理 + 比对\"\n    echo \"\"\n    echo \"清理模式选项:\"\n    echo \"  -v, --verbose      详细输出\"\n    echo \"  -d, --dry-run      只检查，不修改文件\"\n    echo \"  -n, --no-backup    不创建备份文件\"\n    echo \"  -f, --auto-fix     自动修复重复项（保留第一次出现的版本）\"\n    echo \"\"\n    echo \"示例:\"\n    echo \"  $0 -c src/main/resources/strings/strings_zh.xml                    # 清理单个文件\"\n    echo \"  $0 -c -f src/main/resources/strings/strings_zh.xml                 # 自动修复重复项\"\n    echo \"  $0 -m                                                              # 比对中英文文件\"\n    echo \"  $0 -p                                                              # 检查行号一致性\"\n    echo \"  $0 -a -f                                                            # 全功能模式，自动修复\"\n    echo \"  $0 -c -v -d src/main/resources/strings/*.xml                       # 详细检查，不修改\"\n}\n\n# 解析命令行参数\nparse_args() {\n    while [[ $# -gt 0 ]]; do\n        case $1 in\n            -h|--help)\n                show_help\n                exit 0\n                ;;\n            -c|--cleanup)\n                CLEANUP_MODE=true\n                shift\n                ;;\n            -m|--compare)\n                COMPARE_MODE=true\n                shift\n                ;;\n            -a|--all)\n                CLEANUP_MODE=true\n                COMPARE_MODE=true\n                shift\n                ;;\n            -p|--position)\n                POSITION_MODE=true\n                shift\n                ;;\n            -v|--verbose)\n                VERBOSE=true\n                shift\n                ;;\n            -d|--dry-run)\n                DRY_RUN=true\n                shift\n                ;;\n            -n|--no-backup)\n                BACKUP=false\n                shift\n                ;;\n            -f|--auto-fix)\n                AUTO_FIX=true\n                shift\n                ;;\n            -*)\n                echo -e \"${RED}错误: 未知选项 $1${NC}\"\n                show_help\n                exit 1\n                ;;\n            *)\n                FILES+=(\"$1\")\n                shift\n                ;;\n        esac\n    done\n}\n\n# 检查文件是否存在\ncheck_file() {\n    local file=\"$1\"\n    if [[ ! -f \"$file\" ]]; then\n        echo -e \"${RED}错误: 文件不存在: $file${NC}\"\n        return 1\n    fi\n    \n    if [[ ! -r \"$file\" ]]; then\n        echo -e \"${RED}错误: 文件不可读: $file${NC}\"\n        return 1\n    fi\n    \n    return 0\n}\n\n# 验证XML格式\nvalidate_xml() {\n    local file=\"$1\"\n    if ! xmllint --noout \"$file\" 2>/dev/null; then\n        echo -e \"${RED}警告: $file 不是有效的XML文件${NC}\"\n        return 1\n    fi\n    return 0\n}\n\n# 创建备份文件\ncreate_backup() {\n    local file=\"$1\"\n    if [[ \"$BACKUP\" == true && \"$DRY_RUN\" == false ]]; then\n        local timestamp=$(date +\"%Y%m%d_%H%M%S\")\n        local backup_file=\"${file}.backup.${timestamp}\"\n        cp \"$file\" \"$backup_file\"\n        echo -e \"${GREEN}已创建备份: $backup_file${NC}\"\n    fi\n}\n\n# 分析重复的字符串名称\nanalyze_duplicates() {\n    local file=\"$1\"\n    local temp_file=$(mktemp)\n    \n    # 提取所有字符串名称\n    grep -o 'name=\"[^\"]*\"' \"$file\" | sed 's/name=\"//g' | sed 's/\"//g' | sort > \"$temp_file\"\n    \n    # 查找重复项\n    local duplicates=$(sort \"$temp_file\" | uniq -d)\n    \n    if [[ -z \"$duplicates\" ]]; then\n        echo -e \"${GREEN}✓ 没有发现重复的字符串名称${NC}\"\n        rm \"$temp_file\"\n        return 0\n    else\n        echo -e \"${YELLOW}发现 $(echo \"$duplicates\" | wc -l | tr -d ' ') 个重复的字符串名称:${NC}\"\n        echo \"$duplicates\" | head -10\n        if [[ $(echo \"$duplicates\" | wc -l) -gt 10 ]]; then\n            echo \"... 还有 $(($(echo \"$duplicates\" | wc -l) - 10)) 个重复项\"\n        fi\n        \n        if [[ \"$VERBOSE\" == true ]]; then\n            echo \"\"\n            echo \"重复项详情:\"\n            echo \"$duplicates\" | while read name; do\n                echo \"\"\n                echo \"重复项: $name\"\n                grep -n \"name=\\\"$name\\\"\" \"$file\" | while read line; do\n                    echo \"  $line\"\n                done\n            done\n        fi\n        \n        rm \"$temp_file\"\n        return 1\n    fi\n}\n\n# 检查重复的字符串内容\ncheck_content_duplicates() {\n    local file=\"$1\"\n    local temp_file=$(mktemp)\n    \n    # 提取所有字符串内容\n    grep -o '>.*<' \"$file\" | sed 's/^>//' | sed 's/<$//' | sort > \"$temp_file\"\n    \n    # 查找重复内容\n    local duplicates=$(sort \"$temp_file\" | uniq -d)\n    \n    if [[ -z \"$duplicates\" ]]; then\n        echo -e \"${GREEN}✓ 没有发现重复的字符串内容${NC}\"\n        rm \"$temp_file\"\n        return 0\n    else\n        echo -e \"${YELLOW}发现 $(echo \"$duplicates\" | wc -l | tr -d ' ') 个重复的字符串内容:${NC}\"\n        echo \"$duplicates\" | head -10\n        if [[ $(echo \"$duplicates\" | wc -l) -gt 10 ]]; then\n            echo \"... 还有 $(($(echo \"$duplicates\" | wc -l) - 10)) 个重复内容\"\n        fi\n        \n        if [[ \"$VERBOSE\" == true ]]; then\n            echo \"\"\n            echo \"重复内容详情:\"\n            echo \"$duplicates\" | while read content; do\n                echo \"\"\n                echo \"重复内容: $content\"\n                grep -n \">$content<\" \"$file\" | while read line; do\n                    echo \"  $line\"\n                done\n            done\n        fi\n        \n        rm \"$temp_file\"\n        return 0\n    fi\n}\n\n# 自动修复重复项\nauto_fix_duplicates() {\n    local file=\"$1\"\n    local temp_file=$(mktemp)\n    \n    # 获取重复的字符串名称\n    grep -o 'name=\"[^\"]*\"' \"$file\" | sed 's/name=\"//g' | sed 's/\"//g' | sort | uniq -d > \"$temp_file\"\n    \n    if [[ ! -s \"$temp_file\" ]]; then\n        echo -e \"${GREEN}没有重复项需要修复${NC}\"\n        rm \"$temp_file\"\n        return 0\n    fi\n    \n    echo -e \"${BLUE}开始自动修复重复项...${NC}\"\n    \n    # 为每个重复项找到所有行号并删除除第一个之外的所有行\n    while read duplicate_name; do\n        local line_numbers=$(grep -n \"name=\\\"$duplicate_name\\\"\" \"$file\" | cut -d: -f1 | tail -n +2)\n        \n        if [[ -n \"$line_numbers\" ]]; then\n            echo \"$line_numbers\" | while read line_num; do\n                if [[ \"$DRY_RUN\" == true ]]; then\n                    echo -e \"${YELLOW}将删除重复项: $duplicate_name (第${line_num}行)${NC}\"\n                else\n                    sed -i '' \"${line_num}d\" \"$file\"\n                    echo -e \"${GREEN}已删除重复项: $duplicate_name (第${line_num}行)${NC}\"\n                fi\n            done\n        fi\n    done < \"$temp_file\"\n    \n    if [[ \"$DRY_RUN\" == true ]]; then\n        echo -e \"${YELLOW}模拟完成！将删除 $(wc -l < \"$temp_file\") 个重复项${NC}\"\n    else\n        echo -e \"${GREEN}修复完成！共删除 $(wc -l < \"$temp_file\") 个重复项${NC}\"\n    fi\n    \n    rm \"$temp_file\"\n}\n\n# 生成文件统计报告\ngenerate_report() {\n    local file=\"$1\"\n    local total_lines=$(wc -l < \"$file\")\n    local total_strings=$(grep -c 'name=\"[^\"]*\"' \"$file\")\n    local unique_names=$(grep -o 'name=\"[^\"]*\"' \"$file\" | sed 's/name=\"//g' | sed 's/\"//g' | sort | uniq | wc -l)\n    local duplicate_names=$(($total_strings - $unique_names))\n    \n    echo \"=== 文件统计报告 ===\"\n    echo \"文件: $file\"\n    echo \"总行数:      $total_lines\"\n    echo \"字符串总数: $total_strings\"\n    echo \"唯一字符串名称:      $unique_names\"\n    echo \"重复字符串名称:        $duplicate_names\"\n    echo \"\"\n}\n\n# 清理模式主函数\ncleanup_mode() {\n    local has_errors=0\n    \n    # 如果没有指定文件，使用默认的中英文文件\n    if [[ ${#FILES[@]} -eq 0 ]]; then\n        FILES=(\"src/main/resources/strings/strings_zh.xml\" \"src/main/resources/strings/strings_en.xml\")\n    fi\n    \n    for file in \"${FILES[@]}\"; do\n        echo \"处理文件: $file\"\n        echo \"==================================\"\n        \n        if ! check_file \"$file\"; then\n            has_errors=1\n            continue\n        fi\n        \n        if ! validate_xml \"$file\"; then\n            has_errors=1\n            continue\n        fi\n        \n        generate_report \"$file\"\n        \n        local name_duplicates=0\n        local content_duplicates=0\n        \n        analyze_duplicates \"$file\"\n        name_duplicates=$?\n        \n        check_content_duplicates \"$file\"\n        content_duplicates=$?\n        \n        if [[ \"$AUTO_FIX\" == true && $name_duplicates -ne 0 ]]; then\n            if [[ \"$DRY_RUN\" == false ]]; then\n                create_backup \"$file\"\n            fi\n            auto_fix_duplicates \"$file\"\n        fi\n        \n        echo \"处理完成！\"\n        echo \"\"\n    done\n    \n    return $has_errors\n}\n\n# 位置比对模式主函数\nposition_compare_mode() {\n    local zh_file=\"src/main/resources/strings/strings_zh.xml\"\n    local en_file=\"src/main/resources/strings/strings_en.xml\"\n    \n    echo \"=== 中英文字符串资源文件位置比对 ===\"\n    echo \"\"\n    \n    # 检查文件是否存在\n    if [[ ! -f \"$zh_file\" ]]; then\n        echo -e \"${RED}错误: 中文文件不存在: $zh_file${NC}\"\n        return 1\n    fi\n    \n    if [[ ! -f \"$en_file\" ]]; then\n        echo -e \"${RED}错误: 英文文件不存在: $en_file${NC}\"\n        return 1\n    fi\n    \n    # 创建临时文件存储key和行号的映射\n    local zh_temp=$(mktemp)\n    local en_temp=$(mktemp)\n    \n    echo \"1. 提取中文文件中的key和行号...\"\n    grep -n 'name=\"[^\"]*\"' \"$zh_file\" | sed 's/^\\([0-9]*\\):.*name=\"\\([^\"]*\\)\".*/\\1:\\2/' > \"$zh_temp\"\n    \n    echo \"2. 提取英文文件中的key和行号...\"\n    grep -n 'name=\"[^\"]*\"' \"$en_file\" | sed 's/^\\([0-9]*\\):.*name=\"\\([^\"]*\\)\".*/\\1:\\2/' > \"$en_temp\"\n    \n    echo \"3. 统计信息...\"\n    local zh_count=$(wc -l < \"$zh_temp\")\n    local en_count=$(wc -l < \"$en_temp\")\n    echo \"中文文件字符串数量: $zh_count\"\n    echo \"英文文件字符串数量: $en_count\"\n    echo \"\"\n    \n    # 找出共同的key\n    local common_keys=$(comm -12 <(cut -d: -f2 \"$zh_temp\" | sort) <(cut -d: -f2 \"$en_temp\" | sort))\n    local common_count=$(echo \"$common_keys\" | wc -l)\n    echo \"4. 共同key数量: $common_count\"\n    echo \"\"\n    \n    # 检查行号差异\n    local mismatched_count=0\n    local max_diff=0\n    local total_diff=0\n    \n    echo \"5. 检查行号一致性...\"\n    echo \"$common_keys\" | while read key; do\n        local zh_line=$(grep \":$key$\" \"$zh_temp\" | cut -d: -f1)\n        local en_line=$(grep \":$key$\" \"$en_temp\" | cut -d: -f1)\n        \n        if [[ -n \"$zh_line\" && -n \"$en_line\" ]]; then\n            local diff=$((zh_line - en_line))\n            local abs_diff=${diff#-}  # 取绝对值\n            \n            if [[ $abs_diff -gt 0 ]]; then\n                mismatched_count=$((mismatched_count + 1))\n                total_diff=$((total_diff + abs_diff))\n                \n                if [[ $abs_diff -gt $max_diff ]]; then\n                    max_diff=$abs_diff\n                fi\n                \n                if [[ \"$VERBOSE\" == true ]]; then\n                    echo -e \"${YELLOW}行号不匹配: $key${NC}\"\n                    echo \"  中文文件第${zh_line}行\"\n                    echo \"  英文文件第${en_line}行\"\n                    echo \"  差异: $diff 行\"\n                    echo \"\"\n                fi\n            fi\n        fi\n    done\n    \n    # 由于while循环在子shell中执行，我们需要用其他方式统计\n    local actual_mismatched=$(echo \"$common_keys\" | while read key; do\n        local zh_line=$(grep \":$key$\" \"$zh_temp\" | cut -d: -f1)\n        local en_line=$(grep \":$key$\" \"$en_temp\" | cut -d: -f1)\n        \n        if [[ -n \"$zh_line\" && -n \"$en_line\" ]]; then\n            local diff=$((zh_line - en_line))\n            local abs_diff=${diff#-}\n            \n            if [[ $abs_diff -gt 0 ]]; then\n                echo \"1\"\n            fi\n        fi\n    done | wc -l)\n    \n    local actual_total_diff=$(echo \"$common_keys\" | while read key; do\n        local zh_line=$(grep \":$key$\" \"$zh_temp\" | cut -d: -f1)\n        local en_line=$(grep \":$key$\" \"$en_temp\" | cut -d: -f1)\n        \n        if [[ -n \"$zh_line\" && -n \"$en_line\" ]]; then\n            local diff=$((zh_line - en_line))\n            local abs_diff=${diff#-}\n            echo \"$abs_diff\"\n        fi\n    done | awk '{sum+=$1} END {print sum}')\n    \n    local actual_max_diff=$(echo \"$common_keys\" | while read key; do\n        local zh_line=$(grep \":$key$\" \"$zh_temp\" | cut -d: -f1)\n        local en_line=$(grep \":$key$\" \"$en_temp\" | cut -d: -f1)\n        \n        if [[ -n \"$zh_line\" && -n \"$en_line\" ]]; then\n            local diff=$((zh_line - en_line))\n            local abs_diff=${diff#-}\n            echo \"$abs_diff\"\n        fi\n    done | sort -n | tail -1)\n    \n    echo \"6. 位置比对结果:\"\n    if [[ $actual_mismatched -eq 0 ]]; then\n        echo -e \"${GREEN}✅ 所有共同key的行号都一致！${NC}\"\n    else\n        echo -e \"${YELLOW}⚠️  发现 $actual_mismatched 个key的行号不一致${NC}\"\n        echo \"  最大行号差异: $actual_max_diff 行\"\n        echo \"  平均行号差异: $(echo \"scale=1; $actual_total_diff / $actual_mismatched\" | bc 2>/dev/null || echo \"N/A\") 行\"\n        \n        if [[ \"$VERBOSE\" == false ]]; then\n            echo \"\"\n            echo \"使用 -v 选项查看详细的不匹配信息\"\n        fi\n    fi\n    \n    # 清理临时文件\n    rm \"$zh_temp\" \"$en_temp\"\n    \n    echo \"\"\n    echo \"=== 位置比对完成 ===\"\n    echo \"\"\n}\n\n# 比对模式主函数\ncompare_mode() {\n    local zh_file=\"src/main/resources/strings/strings_zh.xml\"\n    local en_file=\"src/main/resources/strings/strings_en.xml\"\n    \n    echo \"=== 中英文字符串资源文件比对 ===\"\n    echo \"\"\n    \n    # 检查文件是否存在\n    if [[ ! -f \"$zh_file\" ]]; then\n        echo -e \"${RED}错误: 中文文件不存在: $zh_file${NC}\"\n        return 1\n    fi\n    \n    if [[ ! -f \"$en_file\" ]]; then\n        echo -e \"${RED}错误: 英文文件不存在: $en_file${NC}\"\n        return 1\n    fi\n    \n    # 提取所有字符串名称\n    echo \"1. 提取中文文件中的字符串名称...\"\n    local zh_names=$(grep -o 'name=\"[^\"]*\"' \"$zh_file\" | sed 's/name=\"//g' | sed 's/\"//g' | sort)\n    \n    echo \"2. 提取英文文件中的字符串名称...\"\n    local en_names=$(grep -o 'name=\"[^\"]*\"' \"$en_file\" | sed 's/name=\"//g' | sed 's/\"//g' | sort)\n    \n    echo \"3. 统计信息...\"\n    local zh_count=$(echo \"$zh_names\" | wc -l)\n    local en_count=$(echo \"$en_names\" | wc -l)\n    \n    echo \"中文文件字符串数量: $zh_count\"\n    echo \"英文文件字符串数量: $en_count\"\n    echo \"\"\n    \n    # 找出中文有但英文没有的字符串\n    echo \"4. 中文有但英文没有的字符串:\"\n    local missing_in_en=$(comm -23 <(echo \"$zh_names\") <(echo \"$en_names\"))\n    if [[ -z \"$missing_in_en\" ]]; then\n        echo -e \"${GREEN}✅ 没有缺失的英文翻译${NC}\"\n    else\n        echo \"$missing_in_en\" | nl\n    fi\n    echo \"\"\n    \n    # 找出英文有但中文没有的字符串\n    echo \"5. 英文有但中文没有的字符串:\"\n    local missing_in_zh=$(comm -13 <(echo \"$zh_names\") <(echo \"$en_names\"))\n    if [[ -z \"$missing_in_zh\" ]]; then\n        echo -e \"${GREEN}✅ 没有缺失的中文翻译${NC}\"\n    else\n        echo \"$missing_in_zh\" | nl\n    fi\n    echo \"\"\n    \n    # 显示具体的缺失内容\n    if [[ ! -z \"$missing_in_en\" ]]; then\n        echo \"6. 缺失的英文翻译详情:\"\n        echo \"$missing_in_en\" | while read name; do\n            local zh_line=$(grep \"name=\\\"$name\\\"\" \"$zh_file\")\n            echo \"字符串名称: $name\"\n            echo \"中文内容: $zh_line\"\n            echo \"---\"\n        done\n    fi\n    \n    if [[ ! -z \"$missing_in_zh\" ]]; then\n        echo \"7. 缺失的中文翻译详情:\"\n        echo \"$missing_in_zh\" | while read name; do\n            local en_line=$(grep \"name=\\\"$name\\\"\" \"$en_file\")\n            echo \"字符串名称: $name\"\n            echo \"英文内容: $en_line\"\n            echo \"---\"\n        done\n    fi\n    \n    echo \"=== 比对完成 ===\"\n    echo \"\"\n}\n\n# 主函数\nmain() {\n    # 初始化文件数组\n    FILES=()\n    \n    # 解析参数\n    parse_args \"$@\"\n    \n    # 检查是否指定了模式\n    if [[ \"$CLEANUP_MODE\" == false && \"$COMPARE_MODE\" == false ]]; then\n        echo -e \"${RED}错误: 请指定操作模式 (-c, -m, 或 -a)${NC}\"\n        show_help\n        exit 1\n    fi\n    \n    # 执行清理模式\n    if [[ \"$CLEANUP_MODE\" == true ]]; then\n        echo \"字符串资源文件清理工具 v2.0\"\n        echo \"\"\n        cleanup_mode\n    fi\n    \n    # 执行比对模式\n    if [[ \"$COMPARE_MODE\" == true ]]; then\n        compare_mode\n    fi\n    \n    echo \"所有操作完成！\"\n}\n\n# 运行主函数\nmain \"$@\"\n"
  },
  {
    "path": "imageprocess/build.gradle.kts",
    "content": "plugins {\n    kotlin(\"jvm\")\n}\n\nrepositories {\n    mavenCentral()\n    maven( \"https://jitpack.io\" )\n}\n\ndependencies {\n    testImplementation(kotlin(\"test\"))\n    implementation (\"org.jetbrains.kotlin:kotlin-stdlib\")\n\n    // coroutines utils\n    implementation (\"com.github.fengzhizi715.Kotlin-Coroutines-Utils:common:${rootProject.extra[\"coroutines.utils\"]}\")\n    implementation (\"org.jetbrains.kotlinx:kotlinx-coroutines-core:${rootProject.extra[\"kotlinx.coroutines.core.version\"]}\")\n\n    // twelvemonkeys\n    implementation(\"com.twelvemonkeys.imageio:imageio-core:${rootProject.extra[\"twelvemonkeys\"]}\")\n    implementation(\"com.twelvemonkeys.imageio:imageio-jpeg:${rootProject.extra[\"twelvemonkeys\"]}\")\n    implementation(\"com.twelvemonkeys.imageio:imageio-hdr:${rootProject.extra[\"twelvemonkeys\"]}\")\n\n    // webp\n    implementation(\"org.sejda.imageio:webp-imageio:0.1.5\")\n\n    // svg\n    implementation(\"org.apache.xmlgraphics:batik-transcoder:${rootProject.extra[\"batik\"]}\")\n    implementation(\"org.apache.xmlgraphics:batik-codec:${rootProject.extra[\"batik\"]}\")\n    implementation(\"org.apache.xmlgraphics:batik-dom:${rootProject.extra[\"batik\"]}\")\n    implementation(\"org.apache.xmlgraphics:batik-svggen:${rootProject.extra[\"batik\"]}\")\n    implementation(\"org.apache.xmlgraphics:batik-parser:${rootProject.extra[\"batik\"]}\")\n}\n\ntasks.test {\n    useJUnitPlatform()\n}\nkotlin {\n    jvmToolchain(17)\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/BufferedImages.kt",
    "content": "package cn.netdiscovery.monica.imageprocess\n\nimport java.awt.image.BufferedImage\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.BufferedImages\n * @author: Tony Shen\n * @date: 2024/5/7 10:46\n * @version: V1.0 <描述当前版本功能>\n */\n\ndata class ImageInfo(val width:Int, val height:Int, val byteArray:ByteArray) {\n    override fun equals(other: Any?): Boolean {\n        if (this === other) return true\n        if (javaClass != other?.javaClass) return false\n\n        other as ImageInfo\n\n        if (width != other.width) return false\n        if (height != other.height) return false\n        return byteArray.contentEquals(other.byteArray)\n    }\n\n    override fun hashCode(): Int {\n        var result = width\n        result = 31 * result + height\n        result = 31 * result + byteArray.contentHashCode()\n        return result\n    }\n}\n\nclass BufferedImages {\n\n    companion object {\n        fun create(width: Int, height: Int, type: Int): BufferedImage =\n            BufferedImage(\n                if (width > 0) width else 1,\n                if (height > 0) height else 1,\n                type)\n\n        fun toBufferedImage(pixels: IntArray, width: Int, height: Int, type: Int): BufferedImage {\n            val bi = BufferedImage(width, height, type)\n            bi.setRGB(0, 0, width, height, pixels, 0, width)\n            return bi\n        }\n    }\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/Colormap.kt",
    "content": "package cn.netdiscovery.monica.imageprocess\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.Colormap\n * @author: Tony Shen\n * @date:  2025/3/22 15:05\n * @version: V1.0 <描述当前版本功能>\n */\ninterface Colormap {\n    /**\n     * Convert a value in the range 0..1 to an RGB color.\n     * @param v a value in the range 0..1\n     * @return an RGB color\n     */\n    fun getColor(v: Float): Int\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/IntIntegralImage.kt",
    "content": "package cn.netdiscovery.monica.imageprocess\n\nimport java.util.*\n\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.IntIntegralImage\n * @author: Tony Shen\n * @date: 2024/6/22 22:30\n * @version: V1.0 <描述当前版本功能>\n */\n\nclass IntIntegralImage {\n\n    // sum index tables\n    private lateinit var sum: IntArray\n    private lateinit var squaresum: FloatArray\n    private lateinit var image: ByteArray\n    private var width = 0\n    private var height = 0\n\n    fun getImage(): ByteArray = image\n\n    fun setImage(image: ByteArray) {\n        this.image = image\n    }\n\n    fun getBlockSum(x1: Int, y1: Int, x2: Int, y2: Int): Int {\n        val tl = sum[y1 * width + x1]\n        val tr = sum[y2 * width + x1]\n        val bl = sum[y1 * width + x2]\n        val br = sum[y2 * width + x2]\n        return br - bl - tr + tl\n    }\n\n    fun getBlockSquareSum(x1: Int, y1: Int, x2: Int, y2: Int): Float {\n        val tl = squaresum[y1 * width + x1]\n        val tr = squaresum[y2 * width + x1]\n        val bl = squaresum[y1 * width + x2]\n        val br = squaresum[y2 * width + x2]\n        return br - bl - tr + tl\n    }\n\n    fun calculate(w: Int, h: Int) {\n        // 初始化积分图\n        width = w + 1\n        height = h + 1\n        sum = IntArray(width * height)\n        Arrays.fill(sum, 0)\n        // 计算积分图\n        var p1 = 0\n        var p2 = 0\n        var p3 = 0\n        var p4: Int\n        for (row in 1 until height) {\n            for (col in 1 until width) {\n                // 计算和查找表\n                p1 = image[(row - 1) * w + col - 1].toInt() and 0xff  // p(x, y)\n                p2 = sum[row * width + col - 1]                       // p(x-1, y)\n                p3 = sum[(row - 1) * width + col]                     // p(x, y-1);\n                p4 = sum[(row - 1) * width + col - 1]                 // p(x-1, y-1);\n                sum[row * width + col] = p1 + p2 + p3 - p4\n            }\n        }\n    }\n\n    fun calculate(w: Int, h: Int, sqrtsum: Boolean) {\n        width = w + 1\n        height = h + 1\n        sum = IntArray(width * height)\n        squaresum = FloatArray(width * height)\n        Arrays.fill(sum, 0)\n        Arrays.fill(squaresum, 0f)\n        // rows\n        var p1 = 0\n        var p2 = 0\n        var p3 = 0\n        var p4: Int\n        var sp2 = 0f\n        var sp3 = 0f\n        var sp4 = 0f\n        for (row in 1 until height) {\n            for (col in 1 until width) {\n                // 计算和查找表\n                p1 = image[(row - 1) * w + col - 1].toInt() and 0xff   // p(x, y)\n                p2 = sum[row * width + col - 1]                        // p(x-1, y)\n                p3 = sum[(row - 1) * width + col]                      // p(x, y-1);\n                p4 = sum[(row - 1) * width + col - 1]                  // p(x-1, y-1);\n                sum[row * width + col] = p1 + p2 + p3 - p4\n\n                // 计算平方查找表\n                sp2 = squaresum[row * width + col - 1]                 // p(x-1, y)\n                sp3 = squaresum[(row - 1) * width + col]               // p(x, y-1);\n                sp4 = squaresum[(row - 1) * width + col - 1]           // p(x-1, y-1);\n                squaresum[row * width + col] = p1 * p1 + sp2 + sp3 - sp4\n            }\n        }\n    }\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/Transformer.kt",
    "content": "package cn.netdiscovery.monica.imageprocess\n\nimport java.awt.image.BufferedImage\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.Transformer\n * @author: Tony Shen\n * @date: 2024/4/27 13:32\n * @version: V1.0 图像转换的接口\n */\ninterface Transformer {\n\n    fun transform(image: BufferedImage): BufferedImage\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/domain/ArrayColormap.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.domain\n\nimport cn.netdiscovery.monica.imageprocess.Colormap\nimport cn.netdiscovery.monica.imageprocess.math.mixColors\n\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.domain.ArrayColormap\n * @author: Tony Shen\n * @date:  2025/3/22 15:07\n * @version: V1.0 <描述当前版本功能>\n */\nopen class ArrayColormap(var map: IntArray = IntArray(256)) : Colormap, Cloneable {\n\n    /**\n     * Convert a value in the range 0..1 to an RGB color.\n     * @param v a value in the range 0..1\n     * @return an RGB color\n     * @see .setColor\n     */\n    override fun getColor(v: Float): Int {\n        /*\n\t\tv *= 255;\n\t\tint n = (int)v;\n\t\tfloat f = v-n;\n\t\tif (n < 0)\n\t\t\treturn map[0];\n\t\telse if (n >= 255)\n\t\t\treturn map[255];\n\t\treturn ImageMath.mixColors(f, map[n], map[n+1]);\n*/\n        var n = (v * 255).toInt()\n        if (n < 0) n = 0\n        else if (n > 255) n = 255\n        return map[n]\n    }\n\n    /**\n     * Set the color at \"index\" to \"color\". Entries are interpolated linearly from\n     * the existing entries at \"firstIndex\" and \"lastIndex\" to the new entry.\n     * firstIndex < index < lastIndex must hold.\n     * @param index the position to set\n     * @param firstIndex the position of the first color from which to interpolate\n     * @param lastIndex the position of the second color from which to interpolate\n     * @param color the color to set\n     */\n    fun setColorInterpolated(index: Int, firstIndex: Int, lastIndex: Int, color: Int) {\n        val firstColor = map[firstIndex]\n        val lastColor = map[lastIndex]\n        for (i in firstIndex..index) map[i] = mixColors((i - firstIndex).toFloat() / (index - firstIndex), firstColor, color)\n        for (i in index until lastIndex) map[i] = mixColors((i - index).toFloat() / (lastIndex - index), color, lastColor)\n    }\n\n    /**\n     * Set a range of the colormap, interpolating between two colors.\n     * @param firstIndex the position of the first color\n     * @param lastIndex the position of the second color\n     * @param color1 the first color\n     * @param color2 the second color\n     */\n    fun setColorRange(firstIndex: Int, lastIndex: Int, color1: Int, color2: Int) {\n        for (i in firstIndex..lastIndex) map[i] = mixColors((i - firstIndex).toFloat() / (lastIndex - firstIndex), color1, color2)\n    }\n\n    /**\n     * Set a range of the colormap to a single color.\n     * @param firstIndex the position of the first color\n     * @param lastIndex the position of the second color\n     * @param color the color\n     */\n    fun setColorRange(firstIndex: Int, lastIndex: Int, color: Int) {\n        for (i in firstIndex..lastIndex) map[i] = color\n    }\n\n    /**\n     * Set one element of the colormap to a given color.\n     * @param index the position of the color\n     * @param color the color\n     * @see .getColor\n     */\n    open fun setColor(index: Int, color: Int) {\n        map[index] = color\n    }\n\n    public override fun clone(): Any {\n//        try {\n            val g = super.clone() as ArrayColormap\n            g.map = map.clone()\n            return g\n//        } catch (e: CloneNotSupportedException) {\n//        }\n//        return null\n    }\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/domain/Gradient.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.domain\n\nimport cn.netdiscovery.monica.imageprocess.math.Noise.Companion.lerp\nimport cn.netdiscovery.monica.imageprocess.math.TWO_PI\nimport cn.netdiscovery.monica.imageprocess.math.mixColors\nimport cn.netdiscovery.monica.imageprocess.math.smoothStep\nimport cn.netdiscovery.monica.imageprocess.utils.clamp\nimport java.awt.Color\nimport kotlin.math.sqrt\n\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.domain.Gradient\n * @author: Tony Shen\n * @date:  2025/3/22 15:13\n * @version: V1.0 <描述当前版本功能>\n */\nclass Gradient : ArrayColormap {\n\n    private var numKnots = 4\n    private var xKnots = intArrayOf(\n        -1, 0, 255, 256\n    )\n    private var yKnots = intArrayOf(\n        -0x1000000, -0x1000000, -0x1, -0x1,\n    )\n    private var knotTypes = byteArrayOf((RGB or SPLINE).toByte(), (RGB or SPLINE).toByte(), (RGB or SPLINE).toByte(), (RGB or SPLINE).toByte())\n\n    /**\n     * Construct a Gradient.\n     */\n    constructor() {\n        rebuildGradient()\n    }\n\n    /**\n     * Construct a Gradient with the given colors.\n     * @param rgb the colors\n     */\n    constructor(rgb: IntArray) : this(null, rgb, null)\n\n    /**\n     * Construct a Gradient with the given colors, knot positions and interpolation types.\n     * @param x the knot positions\n     * @param rgb the colors\n     * @param types interpolation types\n     */\n    /**\n     * Construct a Gradient with the given colors and knot positions.\n     * @param x the knot positions\n     * @param rgb the colors\n     */\n    @JvmOverloads\n    constructor(x: IntArray?, rgb: IntArray, types: ByteArray? = null) {\n        setKnots(x, rgb, types)\n    }\n\n    override fun clone(): Any {\n        val g = super.clone() as Gradient\n        g.map = map.clone()\n        g.xKnots = xKnots.clone()\n        g.yKnots = yKnots.clone()\n        g.knotTypes = knotTypes.clone()\n        return g\n    }\n\n    /**\n     * Copy one Gradient into another.\n     * @param g the Gradient to copy into\n     */\n    fun copyTo(g: Gradient) {\n        g.numKnots = numKnots\n        g.map = map.clone()\n        g.xKnots = xKnots.clone()\n        g.yKnots = yKnots.clone()\n        g.knotTypes = knotTypes.clone()\n    }\n\n    /**\n     * Set a knot color.\n     * @param n the knot index\n     * @param color the color\n     */\n    override fun setColor(n: Int, color: Int) {\n        val firstColor = map[0]\n        val lastColor = map[256 - 1]\n        if (n > 0) for (i in 0 until n) map[i] = mixColors(i.toFloat() / n, firstColor, color)\n        if (n < 256 - 1) for (i in n..255) map[i] = mixColors((i - n).toFloat() / (256 - n), color, lastColor)\n    }\n\n    /**\n     * Get the number of knots in the gradient.\n     * @return the number of knots.\n     */\n    fun getNumKnots(): Int {\n        return numKnots\n    }\n\n    /**\n     * Set a knot color.\n     * @param n the knot index\n     * @param color the color\n     * @see .getKnot\n     */\n    fun setKnot(n: Int, color: Int) {\n        yKnots[n] = color\n        rebuildGradient()\n    }\n\n    /**\n     * Get a knot color.\n     * @param n the knot index\n     * @return the knot color\n     * @see .setKnot\n     */\n    fun getKnot(n: Int): Int {\n        return yKnots[n]\n    }\n\n    /**\n     * Set a knot type.\n     * @param n the knot index\n     * @param type the type\n     * @see .getKnotType\n     */\n    fun setKnotType(n: Int, type: Int) {\n        knotTypes[n] = ((knotTypes[n].toInt() and COLOR_MASK.inv()) or type).toByte()\n        rebuildGradient()\n    }\n\n    /**\n     * Get a knot type.\n     * @param n the knot index\n     * @return the knot type\n     * @see .setKnotType\n     */\n    fun getKnotType(n: Int): Int {\n        return (knotTypes[n].toInt() and COLOR_MASK).toByte().toInt()\n    }\n\n    /**\n     * Set a knot blend type.\n     * @param n the knot index\n     * @param type the knot blend type\n     * @see .getKnotBlend\n     */\n    fun setKnotBlend(n: Int, type: Int) {\n        knotTypes[n] = ((knotTypes[n].toInt() and BLEND_MASK.inv()) or type).toByte()\n        rebuildGradient()\n    }\n\n    /**\n     * Get a knot blend type.\n     * @param n the knot index\n     * @return the knot blend type\n     * @see .setKnotBlend\n     */\n    fun getKnotBlend(n: Int): Byte {\n        return (knotTypes[n].toInt() and BLEND_MASK).toByte()\n    }\n\n    /**\n     * Add a new knot.\n     * @param x the knot position\n     * @param color the color\n     * @param type the knot type\n     * @see .removeKnot\n     */\n    fun addKnot(x: Int, color: Int, type: Int) {\n        val nx = IntArray(numKnots + 1)\n        val ny = IntArray(numKnots + 1)\n        val nt = ByteArray(numKnots + 1)\n        System.arraycopy(xKnots, 0, nx, 0, numKnots)\n        System.arraycopy(yKnots, 0, ny, 0, numKnots)\n        System.arraycopy(knotTypes, 0, nt, 0, numKnots)\n        xKnots = nx\n        yKnots = ny\n        knotTypes = nt\n        // Insert one position before the end so the sort works correctly\n        xKnots[numKnots] = xKnots[numKnots - 1]\n        yKnots[numKnots] = yKnots[numKnots - 1]\n        knotTypes[numKnots] = knotTypes[numKnots - 1]\n        xKnots[numKnots - 1] = x\n        yKnots[numKnots - 1] = color\n        knotTypes[numKnots - 1] = type.toByte()\n        numKnots++\n        sortKnots()\n        rebuildGradient()\n    }\n\n    /**\n     * Remove a knot.\n     * @param n the knot index\n     * @see .addKnot\n     */\n    fun removeKnot(n: Int) {\n        if (numKnots <= 4) return\n        if (n < numKnots - 1) {\n            System.arraycopy(xKnots, n + 1, xKnots, n, numKnots - n - 1)\n            System.arraycopy(yKnots, n + 1, yKnots, n, numKnots - n - 1)\n            System.arraycopy(knotTypes, n + 1, knotTypes, n, numKnots - n - 1)\n        }\n        numKnots--\n        if (xKnots[1] > 0) xKnots[1] = 0\n        rebuildGradient()\n    }\n\n    /**\n     * Set the values of all the knots.\n     * This version does not require the \"extra\" knots at -1 and 256\n     * @param x the knot positions\n     * @param rgb the knot colors\n     * @param types the knot types\n     */\n    fun setKnots(x: IntArray?, rgb: IntArray, types: ByteArray?) {\n        numKnots = rgb.size + 2\n        xKnots = IntArray(numKnots)\n        yKnots = IntArray(numKnots)\n        knotTypes = ByteArray(numKnots)\n        if (x != null) System.arraycopy(x, 0, xKnots, 1, numKnots - 2)\n        else {\n            var i = 1\n            while (i > numKnots - 1) {\n                xKnots[i] = 255 * i / (numKnots - 2)\n                i++\n            }\n        }\n        System.arraycopy(rgb, 0, yKnots, 1, numKnots - 2)\n        if (types != null) System.arraycopy(types, 0, knotTypes, 1, numKnots - 2)\n        else {\n            var i = 0\n            while (i > numKnots) {\n                knotTypes[i] = (RGB or SPLINE).toByte()\n                i++\n            }\n        }\n        sortKnots()\n        rebuildGradient()\n    }\n\n    /**\n     * Set the values of a set of knots.\n     * @param x the knot positions\n     * @param y the knot colors\n     * @param types the knot types\n     * @param offset the first knot to set\n     * @param count the number of knots\n     */\n    fun setKnots(x: IntArray?, y: IntArray?, types: ByteArray?, offset: Int, count: Int) {\n        numKnots = count\n        xKnots = IntArray(numKnots)\n        yKnots = IntArray(numKnots)\n        knotTypes = ByteArray(numKnots)\n        System.arraycopy(x, offset, xKnots, 0, numKnots)\n        System.arraycopy(y, offset, yKnots, 0, numKnots)\n        System.arraycopy(types, offset, knotTypes, 0, numKnots)\n        sortKnots()\n        rebuildGradient()\n    }\n\n    /**\n     * Split a span into two by adding a knot in the middle.\n     * @param n the span index\n     */\n    fun splitSpan(n: Int) {\n        val x = (xKnots[n] + xKnots[n + 1]) / 2\n        addKnot(x, getColor(x / 256.0f), knotTypes[n].toInt())\n        rebuildGradient()\n    }\n\n    /**\n     * Set a knot position.\n     * @param n the knot index\n     * @param x the knot position\n     * @see .setKnotPosition\n     */\n    fun setKnotPosition(n: Int, x: Int) {\n        xKnots[n] = clamp(x, 0, 255)\n        sortKnots()\n        rebuildGradient()\n    }\n\n    /**\n     * Get a knot position.\n     * @param n the knot index\n     * @return the knot position\n     * @see .setKnotPosition\n     */\n    fun getKnotPosition(n: Int): Int {\n        return xKnots[n]\n    }\n\n    /**\n     * Return the knot at a given position.\n     * @param x the position\n     * @return the knot number, or 1 if no knot found\n     */\n    fun knotAt(x: Int): Int {\n        for (i in 1 until numKnots - 1) if (xKnots[i + 1] > x) return i\n        return 1\n    }\n\n    private fun rebuildGradient() {\n        xKnots[0] = -1\n        xKnots[numKnots - 1] = 256\n        yKnots[0] = yKnots[1]\n        yKnots[numKnots - 1] = yKnots[numKnots - 2]\n\n        val knot = 0\n        for (i in 1 until numKnots - 1) {\n            val spanLength = (xKnots[i + 1] - xKnots[i]).toFloat()\n            var end = xKnots[i + 1]\n            if (i == numKnots - 2) end++\n            for (j in xKnots[i] until end) {\n                val rgb1 = yKnots[i]\n                val rgb2 = yKnots[i + 1]\n                val hsb1 = Color.RGBtoHSB((rgb1 shr 16) and 0xff, (rgb1 shr 8) and 0xff, rgb1 and 0xff, null)\n                val hsb2 = Color.RGBtoHSB((rgb2 shr 16) and 0xff, (rgb2 shr 8) and 0xff, rgb2 and 0xff, null)\n                var t = (j - xKnots[i]).toFloat() / spanLength\n                val type = getKnotType(i)\n                val blend = getKnotBlend(i).toInt()\n\n                if (j >= 0 && j <= 255) {\n                    when (blend) {\n                        CONSTANT -> t = 0f\n                        LINEAR -> {}\n                        SPLINE -> //\t\t\t\t\t\tmap[i] = ImageMath.colorSpline(j, numKnots, xKnots, yKnots);\n                            t = smoothStep(0.15f, 0.85f, t)\n\n                        CIRCLE_UP -> {\n                            t = t - 1\n                            t = sqrt((1 - t * t).toDouble()).toFloat()\n                        }\n\n                        CIRCLE_DOWN -> t = 1 - sqrt((1 - t * t).toDouble()).toFloat()\n                    }\n                    when (type) {\n                        RGB -> map[j] = mixColors(t, rgb1, rgb2)\n                        HUE_CW, HUE_CCW -> {\n                            if (type == HUE_CW) {\n                                if (hsb2[0] <= hsb1[0]) hsb2[0] += 1.0f\n                            } else {\n                                if (hsb1[0] <= hsb2[1]) hsb1[0] += 1.0f\n                            }\n                            val h: Float = lerp(t, hsb1[0], hsb2[0]) % (TWO_PI)\n                            val s: Float = lerp(t, hsb1[1], hsb2[1])\n                            val b: Float = lerp(t, hsb1[2], hsb2[2])\n                            map[j] = -0x1000000 or Color.HSBtoRGB(h, s, b) //FIXME-alpha\n                        }\n                    }//\t\t\t\t\t}\n                }\n            }\n        }\n    }\n\n    private fun sortKnots() {\n        for (i in 1 until numKnots - 1) {\n            for (j in 1 until i) {\n                if (xKnots[i] < xKnots[j]) {\n                    var t = xKnots[i]\n                    xKnots[i] = xKnots[j]\n                    xKnots[j] = t\n                    t = yKnots[i]\n                    yKnots[i] = yKnots[j]\n                    yKnots[j] = t\n                    val bt = knotTypes[i]\n                    knotTypes[i] = knotTypes[j]\n                    knotTypes[j] = bt\n                }\n            }\n        }\n    }\n\n    private fun rebuild() {\n        sortKnots()\n        rebuildGradient()\n    }\n\n    /**\n     * Randomize the gradient.\n     */\n    fun randomize() {\n        numKnots = 4 + (6 * Math.random()).toInt()\n        xKnots = IntArray(numKnots)\n        yKnots = IntArray(numKnots)\n        knotTypes = ByteArray(numKnots)\n        for (i in 0 until numKnots) {\n            xKnots[i] = (255 * Math.random()).toInt()\n            yKnots[i] =\n                -0x1000000 or ((255 * Math.random()).toInt() shl 16) or ((255 * Math.random()).toInt() shl 8) or (255 * Math.random()).toInt()\n            knotTypes[i] = (RGB or SPLINE).toByte()\n        }\n        xKnots[0] = -1\n        xKnots[1] = 0\n        xKnots[numKnots - 2] = 255\n        xKnots[numKnots - 1] = 256\n        sortKnots()\n        rebuildGradient()\n    }\n\n    /**\n     * Mutate the gradient.\n     * @param amount the amount in the range zero to one\n     */\n    fun mutate(amount: Float) {\n        for (i in 0 until numKnots) {\n            val rgb = yKnots[i]\n            var r = ((rgb shr 16) and 0xff)\n            var g = ((rgb shr 8) and 0xff)\n            var b = (rgb and 0xff)\n            r = clamp((r + amount * 255 * (Math.random() - 0.5)).toInt())\n            g = clamp((g + amount * 255 * (Math.random() - 0.5)).toInt())\n            b = clamp((b + amount * 255 * (Math.random() - 0.5)).toInt())\n            yKnots[i] = -0x1000000 or (r shl 16) or (g shl 8) or b\n            knotTypes[i] = (RGB or SPLINE).toByte()\n        }\n        sortKnots()\n        rebuildGradient()\n    }\n\n    companion object {\n        /**\n         * Interpolate in RGB space.\n         */\n        const val RGB: Int = 0x00\n\n        /**\n         * Interpolate hue clockwise.\n         */\n        const val HUE_CW: Int = 0x01\n\n        /**\n         * Interpolate hue counter clockwise.\n         */\n        const val HUE_CCW: Int = 0x02\n\n\n        /**\n         * Interpolate linearly.\n         */\n        const val LINEAR: Int = 0x10\n\n        /**\n         * Interpolate using a spline.\n         */\n        const val SPLINE: Int = 0x20\n\n        /**\n         * Interpolate with a rising circle shape curve.\n         */\n        const val CIRCLE_UP: Int = 0x30\n\n        /**\n         * Interpolate with a falling circle shape curve.\n         */\n        const val CIRCLE_DOWN: Int = 0x40\n\n        /**\n         * Don't tnterpolate - just use the starting value.\n         */\n        const val CONSTANT: Int = 0x50\n\n        private const val COLOR_MASK = 0x03\n        private const val BLEND_MASK = 0x70\n\n        /**\n         * Build a random gradient.\n         * @return the new Gradient\n         */\n        fun randomGradient(): Gradient {\n            val g = Gradient()\n            g.randomize()\n            return g\n        }\n    }\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/domain/Histogram.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.domain\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.domain.Histogram\n * @author: Tony Shen\n * @date: 2025/3/20 19:47\n * @version: V1.0 <描述当前版本功能>\n */\nclass Histogram {\n    companion object {\n        const val RED = 0\n        const val GREEN = 1\n        const val BLUE = 2\n        const val GRAY = 3\n    }\n\n    private lateinit var histogram: Array<IntArray>\n    private var numSamples: Int = 0\n    private lateinit var minValue: IntArray\n    private lateinit var maxValue: IntArray\n    private lateinit var minFrequency: IntArray\n    private lateinit var maxFrequency: IntArray\n    private lateinit var mean: FloatArray\n    private var isGray: Boolean = true\n\n    constructor()\n\n    constructor(pixels: IntArray, w: Int, h: Int, offset: Int, stride: Int) {\n        histogram = Array(3) { IntArray(256) }\n        minValue = IntArray(4)\n        maxValue = IntArray(4)\n        minFrequency = IntArray(3)\n        maxFrequency = IntArray(3)\n        mean = FloatArray(3)\n\n        numSamples = w * h\n        isGray = true\n\n        var index: Int\n        for (y in 0 until h) {\n            index = offset + y * stride\n            for (x in 0 until w) {\n                val rgb = pixels[index++]\n                val r = (rgb shr 16) and 0xff\n                val g = (rgb shr 8) and 0xff\n                val b = rgb and 0xff\n                histogram[RED][r]++\n                histogram[GREEN][g]++\n                histogram[BLUE][b]++\n            }\n        }\n\n        for (i in 0 until 256) {\n            if (histogram[RED][i] != histogram[GREEN][i] || histogram[GREEN][i] != histogram[BLUE][i]) {\n                isGray = false\n                break\n            }\n        }\n\n        for (i in 0 until 3) {\n            for (j in 0 until 256) {\n                if (histogram[i][j] > 0) {\n                    minValue[i] = j\n                    break\n                }\n            }\n\n            for (j in 255 downTo 0) {\n                if (histogram[i][j] > 0) {\n                    maxValue[i] = j\n                    break\n                }\n            }\n\n            minFrequency[i] = Int.MAX_VALUE\n            maxFrequency[i] = 0\n            for (j in 0 until 256) {\n                minFrequency[i] = minOf(minFrequency[i], histogram[i][j])\n                maxFrequency[i] = maxOf(maxFrequency[i], histogram[i][j])\n                mean[i] += j * histogram[i][j].toFloat()\n            }\n            mean[i] /= numSamples.toFloat()\n        }\n        minValue[GRAY] = minOf(minValue[RED], minValue[GREEN], minValue[BLUE])\n        maxValue[GRAY] = maxOf(maxValue[RED], maxValue[GREEN], maxValue[BLUE])\n    }\n\n    fun isGray(): Boolean = isGray\n\n    fun getNumSamples(): Int = numSamples\n\n    fun getFrequency(value: Int): Int = if (numSamples > 0 && isGray && value in 0..255) histogram[0][value] else -1\n\n    fun getFrequency(channel: Int, value: Int): Int =\n        if (numSamples < 1 || channel !in 0..2 || value !in 0..255) -1 else histogram[channel][value]\n\n    fun getMinFrequency(): Int = if (numSamples > 0 && isGray) minFrequency[0] else -1\n\n    fun getMinFrequency(channel: Int): Int = if (numSamples < 1 || channel !in 0..2) -1 else minFrequency[channel]\n\n    fun getMaxFrequency(): Int = if (numSamples > 0 && isGray) maxFrequency[0] else -1\n\n    fun getMaxFrequency(channel: Int): Int = if (numSamples < 1 || channel !in 0..2) -1 else maxFrequency[channel]\n\n    fun getMinValue(): Int = if (numSamples > 0 && isGray) minValue[0] else -1\n\n    fun getMinValue(channel: Int): Int = minValue[channel]\n\n    fun getMaxValue(): Int = if (numSamples > 0 && isGray) maxValue[0] else -1\n\n    fun getMaxValue(channel: Int): Int = maxValue[channel]\n\n    fun getMeanValue(): Float = if (numSamples > 0 && isGray) mean[0] else -1.0F\n\n    fun getMeanValue(channel: Int): Float = if (numSamples > 0 && channel in RED..BLUE) mean[channel] else -1.0F\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/filter/BilateralFilter.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.filter\n\nimport cn.netdiscovery.monica.imageprocess.filter.base.BaseFilter\nimport cn.netdiscovery.monica.imageprocess.utils.clamp\nimport java.awt.image.BufferedImage\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.filter.BilateralFilter\n * @author: Tony Shen\n * @date: 2024/4/29 17:21\n * @version: V1.0 <描述当前版本功能>\n */\nclass BilateralFilter(private val ds:Double = 1.0, private val rs:Double = 1.0): BaseFilter() {\n\n    private val factor = -0.5\n    private var radius = 0 // half-length of Gaussian kernel Adobe Photoshop\n\n    private lateinit var cWeightTable: Array<DoubleArray>\n    private lateinit var sWeightTable: DoubleArray\n\n    override fun doFilter(srcImage: BufferedImage, dstImage: BufferedImage): BufferedImage {\n\n        radius = Math.max(ds, rs).toInt()\n        buildDistanceWeightTable()\n        buildSimilarityWeightTable()\n\n        val outPixels = IntArray(width * height)\n\n        var index = 0\n        var redSum = 0.0\n        var greenSum = 0.0\n        var blueSum = 0.0\n        var csRedWeight = 0.0\n        var csGreenWeight = 0.0\n        var csBlueWeight = 0.0\n        var csSumRedWeight = 0.0\n        var csSumGreenWeight = 0.0\n        var csSumBlueWeight = 0.0\n        for (row in 0 until height) {\n            var ta = 0\n            var tr = 0\n            var tg = 0\n            var tb = 0\n            for (col in 0 until width) {\n                index = row * width + col\n                ta = inPixels[index] shr 24 and 0xff\n                tr = inPixels[index] shr 16 and 0xff\n                tg = inPixels[index] shr 8 and 0xff\n                tb = inPixels[index] and 0xff\n                var rowOffset = 0\n                var colOffset = 0\n                var index2 = 0\n                var ta2 = 0\n                var tr2 = 0\n                var tg2 = 0\n                var tb2 = 0\n                for (semirow in -radius..radius) {\n                    for (semicol in -radius..radius) {\n                        rowOffset = if (row + semirow >= 0 && row + semirow < height) {\n                            row + semirow\n                        } else {\n                            0\n                        }\n                        colOffset = if (semicol + col >= 0 && semicol + col < width) {\n                            col + semicol\n                        } else {\n                            0\n                        }\n                        index2 = rowOffset * width + colOffset\n                        ta2 = inPixels[index2] shr 24 and 0xff\n                        tr2 = inPixels[index2] shr 16 and 0xff\n                        tg2 = inPixels[index2] shr 8 and 0xff\n                        tb2 = inPixels[index2] and 0xff\n                        csRedWeight =\n                            cWeightTable[semirow + radius][semicol + radius] * sWeightTable[Math.abs(tr2 - tr)]\n                        csGreenWeight =\n                            cWeightTable[semirow + radius][semicol + radius] * sWeightTable[Math.abs(tg2 - tg)]\n                        csBlueWeight =\n                            cWeightTable[semirow + radius][semicol + radius] * sWeightTable[Math.abs(tb2 - tb)]\n                        csSumRedWeight += csRedWeight\n                        csSumGreenWeight += csGreenWeight\n                        csSumBlueWeight += csBlueWeight\n                        redSum += csRedWeight * tr2.toDouble()\n                        greenSum += csGreenWeight * tg2.toDouble()\n                        blueSum += csBlueWeight * tb2.toDouble()\n                    }\n                }\n                tr = Math.floor(redSum / csSumRedWeight).toInt()\n                tg = Math.floor(greenSum / csSumGreenWeight).toInt()\n                tb = Math.floor(blueSum / csSumBlueWeight).toInt()\n                outPixels[index] = ta shl 24 or (clamp(tr) shl 16) or (clamp(tg) shl 8) or clamp(tb)\n\n                // clean value for next time...\n                blueSum = 0.0\n                greenSum = blueSum\n                redSum = greenSum\n                csBlueWeight = 0.0\n                csGreenWeight = csBlueWeight\n                csRedWeight = csGreenWeight\n                csSumBlueWeight = 0.0\n                csSumGreenWeight = csSumBlueWeight\n                csSumRedWeight = csSumGreenWeight\n            }\n        }\n\n        setRGB(dstImage, 0, 0, width, height, outPixels)\n        return dstImage\n    }\n\n    private fun buildDistanceWeightTable() {\n        val size: Int = 2 * radius + 1\n        cWeightTable = Array<DoubleArray>(size) { DoubleArray(size) }\n        for (semirow in -radius..radius) {\n            for (semicol in -radius..radius) {\n                // calculate Euclidean distance between center point and close pixels\n                val delta = Math.sqrt((semirow * semirow + semicol * semicol).toDouble()) / ds\n                val deltaDelta = delta * delta\n                cWeightTable.get(semirow + radius)[semicol + radius] = Math.exp(deltaDelta * factor)\n            }\n        }\n    }\n\n    /**\n     * for gray image\n     * @param row\n     * @param col\n     * @param inPixels\n     */\n    private fun buildSimilarityWeightTable() {\n        sWeightTable = DoubleArray(256) // since the color scope is 0 ~ 255\n        for (i in 0..255) {\n            val delta = Math.sqrt((i * i).toDouble()) / rs\n            val deltaDelta = delta * delta\n            sWeightTable[i] = Math.exp(deltaDelta * factor)\n        }\n    }\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/filter/BlockFilter.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.filter\n\nimport cn.netdiscovery.monica.imageprocess.filter.base.BaseFilter\nimport java.awt.image.BufferedImage\nimport kotlin.math.max\nimport kotlin.math.min\n\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.filter.BlockFilter\n * @author: Tony Shen\n * @date: 2024/5/4 23:35\n * @version: V1.0 <描述当前版本功能>\n */\nclass BlockFilter(blockSize: Int = 2) : BaseFilter() {\n\n    // blockSize 会被用作 Kotlin range 的 step，必须 > 0\n    private val blockSize: Int = max(1, blockSize)\n\n    override fun doFilter(srcImage: BufferedImage, dstImage: BufferedImage): BufferedImage {\n        val pixels = IntArray(blockSize * blockSize)\n\n        for (y in 0 until height step blockSize){\n            for (x in 0 until width step blockSize){\n                val w = min(blockSize, width - x)\n                val h = min(blockSize, height - y)\n                val t = w * h\n                getRGB(srcImage, x, y, w, h, pixels)\n                var r = 0\n                var g = 0\n                var b = 0\n                var argb: Int\n                var i = 0\n                for (by in 0 until h) {\n                    for (bx in 0 until w) {\n                        argb = pixels[i]\n                        r += argb shr 16 and 0xff\n                        g += argb shr 8 and 0xff\n                        b += argb and 0xff\n                        i++\n                    }\n                }\n                argb = r / t shl 16 or (g / t shl 8) or b / t\n                i = 0\n                for (by in 0 until h) {\n                    for (bx in 0 until w) {\n                        pixels[i] = pixels[i] and -0x1000000 or argb\n                        i++\n                    }\n                }\n                setRGB(dstImage, x, y, w, h, pixels)\n            }\n        }\n\n        return dstImage\n    }\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/filter/BumpFilter.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.filter\n\nimport cn.netdiscovery.monica.imageprocess.filter.base.ConvolveFilter\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.filter.BumpFilter\n * @author: Tony Shen\n * @date: 2024/5/5 18:06\n * @version: V1.0 <描述当前版本功能>\n */\n\nclass BumpFilter : ConvolveFilter(embossMatrix) {\n\n    companion object {\n        private val embossMatrix = floatArrayOf(\n            -1.0f, -1.0f, 0.0f,\n            -1.0f, 1.0f, 1.0f,\n            0.0f, 1.0f, 1.0f\n        )\n    }\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/filter/CarveFilter.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.filter\n\nimport cn.netdiscovery.monica.imageprocess.filter.base.ColorProcessorFilter\nimport cn.netdiscovery.monica.imageprocess.utils.clamp\nimport java.awt.image.BufferedImage\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.filter.CarveFilter\n * @author: Tony Shen\n * @date: 2025/3/17 12:36\n * @version: V1.0 <描述当前版本功能>\n */\nclass CarveFilter: ColorProcessorFilter()  {\n\n    override fun doColorProcessor(dstImage: BufferedImage): BufferedImage {\n        val output = Array(3) { ByteArray(R.size) }\n\n        var index = 0\n        for (row in 1..<height - 1) {\n            val ta = 0\n            var tr = 0\n            var tg = 0\n            var tb = 0\n            for (col in 1..<width - 1) {\n                // Index of the pixel in the array\n                index = row * width + col\n                val bidx = row * width + (col - 1)\n                val aidx = row * width + (col + 1)\n\n                val br = R[bidx].toInt() and 0xff\n                val bg = G[bidx].toInt() and 0xff\n                val bb = B[bidx].toInt() and 0xff\n\n                val ar = R[aidx].toInt() and 0xff\n                val ag = G[aidx].toInt() and 0xff\n                val ab = B[aidx].toInt() and 0xff\n\n                // calculate new RGB value\n                tr = ar - br + 128\n                tg = ag - bg + 128\n                tb = ab - bb + 128\n\n                output[0][index] = clamp(tr).toByte()\n                output[1][index] = clamp(tg).toByte()\n                output[2][index] = clamp(tb).toByte()\n            }\n        }\n\n        R = output[0]\n        G = output[1]\n        B = output[2]\n\n        return toBufferedImage(dstImage)\n    }\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/filter/CellularFilter.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.filter\n\nimport cn.netdiscovery.monica.imageprocess.Colormap\nimport cn.netdiscovery.monica.imageprocess.domain.Gradient\nimport cn.netdiscovery.monica.imageprocess.filter.base.WholeImageFilter\nimport cn.netdiscovery.monica.imageprocess.math.Function2D\nimport cn.netdiscovery.monica.imageprocess.math.Noise\nimport cn.netdiscovery.monica.imageprocess.math.mixColors\nimport cn.netdiscovery.monica.imageprocess.math.smoothStep\nimport cn.netdiscovery.monica.imageprocess.utils.clamp\nimport java.awt.Rectangle\nimport java.util.*\nimport kotlin.math.*\n\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.filter.CellularFilter\n * @author: Tony Shen\n * @date:  2025/3/22 14:53\n * @version: V1.0 <描述当前版本功能>\n */\nopen class CellularFilter(open var angle: Double = 0.0,\n                          open var scale: Float = 32f,\n                          open var randomness: Float = 0f,\n                          open var gridType: Int = HEXAGONAL) : WholeImageFilter(), Function2D, Cloneable {\n\n    companion object {\n        private var probabilities: ByteArray? = null\n        const val RANDOM: Int = 0\n        const val SQUARE: Int = 1\n        const val HEXAGONAL: Int = 2\n        const val OCTAGONAL: Int = 3\n        const val TRIANGULAR: Int = 4\n    }\n\n    protected var stretch: Float = 1.0f\n    var amount: Float = 1.0f\n    var turbulence: Float = 1.0f\n    var gain: Float = 0.5f\n    var bias: Float = 0.5f\n    var distancePower: Float = 2f\n    var useColor: Boolean = false\n    protected var colormap: Colormap = Gradient()\n    protected var coefficients: FloatArray = floatArrayOf(1f, 0f, 0f, 0f)\n    protected var angleCoefficient: Float = 0f\n    protected var random: Random = Random()\n    protected var m00: Float = 1.0f\n    protected var m01: Float = 0.0f\n    protected var m10: Float = 0.0f\n    protected var m11: Float = 1.0f\n    protected var results: Array<Point?>\n\n    private val min = 0f\n    private val max = 0f\n    private var gradientCoefficient = 0f\n\n    init {\n        results = arrayOfNulls(3)\n        for (j in results.indices) results[j] = Point()\n        if (probabilities == null) {\n            probabilities = ByteArray(8192)\n            var factorial = 1f\n            var total = 0f\n            val mean = 2.5f\n            for (i in 0..9) {\n                if (i > 1) factorial *= i.toFloat()\n                val probability = mean.pow(i) * exp(-mean.toDouble()).toFloat() / factorial\n                val start = (total * 8192).toInt()\n                total += probability\n                val end = (total * 8192).toInt()\n                for (j in start until end) probabilities!![j] = i.toByte()\n            }\n        }\n\n        val cos = cos(angle).toFloat()\n        val sin = sin(angle).toFloat()\n        m00 = cos\n        m01 = sin\n        m10 = -sin\n        m11 = cos\n    }\n\n    inner class Point {\n        var index: Int = 0\n        var x: Float = 0f\n        var y: Float = 0f\n        var dx: Float = 0f\n        var dy: Float = 0f\n        var cubeX: Float = 0f\n        var cubeY: Float = 0f\n        var distance: Float = 0f\n    }\n\n    private fun checkCube(x: Float, y: Float, cubeX: Int, cubeY: Int, results: Array<Point?>?): Float {\n        random.setSeed((571 * cubeX + 23 * cubeY).toLong())\n        val numPoints = when (gridType) {\n            RANDOM -> probabilities!![random.nextInt() and 0x1fff].toInt()\n            SQUARE -> 1\n            HEXAGONAL -> 1\n            OCTAGONAL -> 2\n            TRIANGULAR -> 2\n            else -> probabilities!![random.nextInt() and 0x1fff].toInt()\n        }\n        for (i in 0 until numPoints) {\n            var px = 0f\n            var py = 0f\n            var weight = 1.0f\n            when (gridType) {\n                RANDOM -> {\n                    px = random.nextFloat()\n                    py = random.nextFloat()\n                }\n\n                SQUARE -> {\n                    py = 0.5f\n                    px = py\n                    if (randomness != 0f) {\n                        px += (randomness * (random.nextFloat() - 0.5)).toFloat()\n                        py += (randomness * (random.nextFloat() - 0.5)).toFloat()\n                    }\n                }\n\n                HEXAGONAL -> {\n                    if ((cubeX and 1) == 0) {\n                        px = 0.75f\n                        py = 0f\n                    } else {\n                        px = 0.75f\n                        py = 0.5f\n                    }\n                    if (randomness != 0f) {\n                        px += randomness * Noise.noise2(271 * (cubeX + px), 271 * (cubeY + py))\n                        py += randomness * Noise.noise2(271 * (cubeX + px) + 89, 271 * (cubeY + py) + 137)\n                    }\n                }\n\n                OCTAGONAL -> {\n                    when (i) {\n                        0 -> {\n                            px = 0.207f\n                            py = 0.207f\n                        }\n\n                        1 -> {\n                            px = 0.707f\n                            py = 0.707f\n                            weight = 1.6f\n                        }\n                    }\n                    if (randomness != 0f) {\n                        px += randomness * Noise.noise2(271 * (cubeX + px), 271 * (cubeY + py))\n                        py += randomness * Noise.noise2(271 * (cubeX + px) + 89, 271 * (cubeY + py) + 137)\n                    }\n                }\n\n                TRIANGULAR -> {\n                    if ((cubeY and 1) == 0) {\n                        if (i == 0) {\n                            px = 0.25f\n                            py = 0.35f\n                        } else {\n                            px = 0.75f\n                            py = 0.65f\n                        }\n                    } else {\n                        if (i == 0) {\n                            px = 0.75f\n                            py = 0.35f\n                        } else {\n                            px = 0.25f\n                            py = 0.65f\n                        }\n                    }\n                    if (randomness != 0f) {\n                        px += randomness * Noise.noise2(271 * (cubeX + px), 271 * (cubeY + py))\n                        py += randomness * Noise.noise2(271 * (cubeX + px) + 89, 271 * (cubeY + py) + 137)\n                    }\n                }\n            }\n            var dx = abs((x - px).toDouble()).toFloat()\n            var dy = abs((y - py).toDouble()).toFloat()\n            dx *= weight\n            dy *= weight\n            var d = if (distancePower == 1.0f) dx + dy\n            else if (distancePower == 2.0f) sqrt((dx * dx + dy * dy).toDouble()).toFloat()\n            else (dx.pow(distancePower) + dy.pow(distancePower)).pow((1 / distancePower))\n\n            // Insertion sort the long way round to speed it up a bit\n            if (d < results!![0]!!.distance) {\n                val p = results[2]\n                results[2] = results[1]\n                results[1] = results[0]\n                results[0] = p\n                p!!.distance = d\n                p.dx = dx\n                p.dy = dy\n                p.x = cubeX + px\n                p.y = cubeY + py\n            } else if (d < results[1]!!.distance) {\n                val p = results[2]\n                results[2] = results[1]\n                results[1] = p\n                p!!.distance = d\n                p.dx = dx\n                p.dy = dy\n                p.x = cubeX + px\n                p.y = cubeY + py\n            } else if (d < results[2]!!.distance) {\n                val p = results[2]\n                p!!.distance = d\n                p.dx = dx\n                p.dy = dy\n                p.x = cubeX + px\n                p.y = cubeY + py\n            }\n        }\n        return results!![2]!!.distance\n    }\n\n    override fun evaluate(x: Float, y: Float): Float {\n        for (j in results.indices)\n            results[j]!!.distance = Float.POSITIVE_INFINITY\n\n        val ix = x.toInt()\n        val iy = y.toInt()\n        val fx = x - ix\n        val fy = y - iy\n\n        var d = checkCube(fx, fy, ix, iy, results)\n        if (d > fy) d = checkCube(fx, fy + 1, ix, iy - 1, results)\n        if (d > 1 - fy) d = checkCube(fx, fy - 1, ix, iy + 1, results)\n        if (d > fx) {\n            checkCube(fx + 1, fy, ix - 1, iy, results)\n            if (d > fy) d = checkCube(fx + 1, fy + 1, ix - 1, iy - 1, results)\n            if (d > 1 - fy) d = checkCube(fx + 1, fy - 1, ix - 1, iy + 1, results)\n        }\n        if (d > 1 - fx) {\n            d = checkCube(fx - 1, fy, ix + 1, iy, results)\n            if (d > fy) d = checkCube(fx - 1, fy + 1, ix + 1, iy - 1, results)\n            if (d > 1 - fy) d = checkCube(fx - 1, fy - 1, ix + 1, iy + 1, results)\n        }\n\n        var t = 0f\n        for (i in 0..2) t += coefficients[i] * results!![i]!!.distance\n        if (angleCoefficient != 0f) {\n            var angle =\n                atan2((y - results!![0]!!.y).toDouble(), (x - results!![0]!!.x).toDouble()).toFloat()\n            if (angle < 0) angle += 2 * Math.PI.toFloat()\n            angle /= 4 * Math.PI.toFloat()\n            t += angleCoefficient * angle\n        }\n        if (gradientCoefficient != 0f) {\n            val a = 1 / (results!![0]!!.dy + results!![0]!!.dx)\n            t += gradientCoefficient * a\n        }\n        return t\n    }\n\n    private fun turbulence2(x: Float, y: Float, freq: Float): Float {\n        var t = 0.0f\n\n        var f = 1.0f\n        while (f <= freq) {\n            t += evaluate(f * x, f * y) / f\n            f *= 2f\n        }\n        return t\n    }\n\n    open fun getPixel(x: Int, y: Int, inPixels: IntArray, width: Int, height: Int): Int {\n        var nx = m00 * x + m01 * y\n        var ny = m10 * x + m11 * y\n        nx /= scale\n        ny /= scale * stretch\n        nx += 1000f\n        ny += 1000f // Reduce artifacts around 0,0\n        var f = if (turbulence == 1.0f) evaluate(nx, ny) else turbulence2(nx, ny, turbulence)\n        // Normalize to 0..1\n//\t\tf = (f-min)/(max-min);\n        f *= 2f\n        f *= amount\n        val a = 0xff000000.toInt()\n        var v: Int\n        if (colormap != null) {\n            v = colormap.getColor(f)\n            if (useColor) {\n                val srcx: Int = clamp(((results[0]!!.x - 1000) * scale).toInt(), 0, width - 1)\n                val srcy: Int = clamp(((results[0]!!.y - 1000) * scale).toInt(), 0, height - 1)\n                v = inPixels[srcy * width + srcx]\n                f =\n                    (results[1]!!.distance - results[0]!!.distance) / (results[1]!!.distance + results[0]!!.distance)\n                f = smoothStep(coefficients[1], coefficients[0], f)\n                v = mixColors(f, 0xff000000.toInt(), v)\n            }\n            return v\n        } else {\n            v = clamp((f * 255).toInt())\n            val r = v shl 16\n            val g = v shl 8\n            val b = v\n            return a or r or g or b\n        }\n    }\n\n    override fun filterPixels(width: Int, height: Int, inPixels: IntArray, transformedSpace: Rectangle): IntArray {\n//\t\tfloat[] minmax = Noise.findRange(this, null);\n//\t\tmin = minmax[0];\n//\t\tmax = minmax[1];\n\n        var index = 0\n        val outPixels = IntArray(width * height)\n\n        for (y in 0 until height) {\n            for (x in 0 until width) {\n                outPixels[index++] = getPixel(x, y, inPixels, width, height)\n            }\n        }\n        return outPixels\n    }\n\n    public override fun clone(): Any {\n        val f = super.clone() as CellularFilter\n        f.coefficients = coefficients.clone()\n        f.results = results.clone()\n        f.random = Random()\n        return f\n    }\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/filter/ColorFilter.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.filter\n\nimport cn.netdiscovery.monica.imageprocess.filter.base.ColorProcessorFilter\nimport cn.netdiscovery.monica.imageprocess.lut.*\nimport java.awt.image.BufferedImage\n\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.filter.ColorFilter\n * @author: Tony Shen\n * @date: 2024/6/17 14:23\n * @version: V1.0 <描述当前版本功能>\n */\n\nclass ColorFilter(val style: Int = 0) : ColorProcessorFilter() {\n\n    private fun getStyleLUT(style: Int): Array<IntArray> = getColorFilterLUT(style)\n\n    override fun doColorProcessor(dstImage: BufferedImage): BufferedImage {\n        var tr = 0\n        var tg = 0\n        var tb = 0\n        val lut = getStyleLUT(style)\n        val size: Int = R.size\n        for (i in 0 until size) {\n            tr = R[i].toInt() and 0xff\n            tg = G[i].toInt() and 0xff\n            tb = B[i].toInt() and 0xff\n            R[i] = lut[tr][0].toByte()\n            G[i] = lut[tg][1].toByte()\n            B[i] = lut[tb][2].toByte()\n        }\n\n        return toBufferedImage(dstImage)\n    }\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/filter/ColorHalftoneFilter.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.filter\n\nimport cn.netdiscovery.monica.imageprocess.filter.base.BaseFilter\nimport cn.netdiscovery.monica.imageprocess.math.mod\nimport cn.netdiscovery.monica.imageprocess.math.smoothStep\nimport cn.netdiscovery.monica.imageprocess.utils.clamp\nimport java.awt.image.BufferedImage\nimport kotlin.math.cos\nimport kotlin.math.min\nimport kotlin.math.sin\nimport kotlin.math.sqrt\n\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.filter.ColorHalftoneFilter\n * @author: Tony Shen\n * @date: 2025/3/19 13:36\n * @version: V1.0 <描述当前版本功能>\n */\nclass ColorHalftoneFilter(private val dotRadius:Float = 2f): BaseFilter() {\n\n    private val cyanScreenAngle = Math.toRadians(108.0).toFloat()\n    private val magentaScreenAngle = Math.toRadians(162.0).toFloat()\n    private val yellowScreenAngle = Math.toRadians(90.0).toFloat()\n\n    override fun doFilter(srcImage: BufferedImage, dstImage: BufferedImage): BufferedImage {\n        val gridSize = 2 * dotRadius * 1.414f\n        val angles = floatArrayOf(cyanScreenAngle, magentaScreenAngle, yellowScreenAngle)\n        val mx = floatArrayOf(0f, -1f, 1f, 0f, 0f)\n        val my = floatArrayOf(0f, 0f, 0f, -1f, 1f)\n        val halfGridSize = gridSize / 2\n        val outPixels = IntArray(width)\n        val inPixels = getRGB(srcImage, 0, 0, width, height, null)\n        for (y in 0..<height) {\n            var x = 0\n            var ix = y * width\n            while (x < width) {\n                outPixels[x] = (inPixels[ix] and -0x1000000) or 0xffffff\n                x++\n                ix++\n            }\n            for (channel in 0..2) {\n                val shift = 16 - 8 * channel\n                val mask = 0x000000ff shl shift\n                val angle = angles[channel]\n                val sin = sin(angle.toDouble()).toFloat()\n                val cos = cos(angle.toDouble()).toFloat()\n\n                for (x in 0..<width) {\n                    // Transform x,y into halftone screen coordinate space\n                    var tx = x * cos + y * sin\n                    var ty = -x * sin + y * cos\n\n\n                    // Find the nearest grid point\n                    tx = tx - mod(tx - halfGridSize, gridSize) + halfGridSize\n                    ty = ty - mod(ty - halfGridSize, gridSize) + halfGridSize\n\n                    var f = 1f\n\n                    // TODO: Efficiency warning: Because the dots overlap, we need to check neighbouring grid squares.\n                    // We check all four neighbours, but in practice only one can ever overlap any given point.\n                    for (i in 0..4) {\n                        // Find neigbouring grid point\n                        val ttx = tx + mx[i] * gridSize\n                        val tty = ty + my[i] * gridSize\n                        // Transform back into image space\n                        val ntx = ttx * cos - tty * sin\n                        val nty = ttx * sin + tty * cos\n                        // Clamp to the image\n                        val nx: Int = clamp(ntx.toInt(), 0, width - 1)\n                        val ny: Int = clamp(nty.toInt(), 0, height - 1)\n                        val argb = inPixels[ny * width + nx]\n                        val nr = (argb shr shift) and 0xff\n                        var l = nr / 255.0f\n                        l = 1 - l * l\n                        l *= (halfGridSize * 1.414).toFloat()\n                        val dx = x - ntx\n                        val dy = y - nty\n                        val dx2 = dx * dx\n                        val dy2 = dy * dy\n                        val R = sqrt((dx2 + dy2).toDouble()).toFloat()\n                        val f2: Float = 1 - smoothStep(R, R + 1, l)\n                        f = min(f.toDouble(), f2.toDouble()).toFloat()\n                    }\n\n                    var v = (255 * f).toInt()\n                    v = v shl shift\n                    v = v xor mask.inv()\n                    v = v or -0x1000000\n                    outPixels[x] = outPixels[x] and v\n                }\n            }\n            setRGB(dstImage, 0, y, width, 1, outPixels)\n        }\n\n        return dstImage\n    }\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/filter/ConBriFilter.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.filter\n\nimport cn.netdiscovery.monica.imageprocess.filter.base.ColorProcessorFilter\nimport cn.netdiscovery.monica.imageprocess.utils.clamp\nimport java.awt.image.BufferedImage\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.filter.ConBriFilter\n * @author: Tony Shen\n * @date: 2024/4/27 13:57\n * @version: V1.0 <描述当前版本功能>\n */\nclass ConBriFilter(private val contrast:Float = 1.5f,private val brightness:Float =1.0f): ColorProcessorFilter() {\n\n    override fun doColorProcessor(dstImage: BufferedImage): BufferedImage {\n        // calculate RED, GREEN, BLUE means of pixel\n        var index = 0\n        val rgbmeans = IntArray(3)\n        var redSum = 0.0\n        var greenSum = 0.0\n        var blueSum = 0.0\n        val total = size.toDouble()\n        for (row in 0 until height) {\n            var tr = 0\n            var tg = 0\n            var tb = 0\n            for (col in 0 until width) {\n                index = row * width + col\n\n                tr = R[index].toInt() and 0xff\n                tg = G[index].toInt() and 0xff\n                tb = B[index].toInt() and 0xff\n                redSum += tr.toDouble()\n                greenSum += tg.toDouble()\n                blueSum += tb.toDouble()\n            }\n        }\n\n        rgbmeans[0] = (redSum / total).toInt()\n        rgbmeans[1] = (greenSum / total).toInt()\n        rgbmeans[2] = (blueSum / total).toInt()\n\n        // adjust contrast and brightness algorithm, here\n        for (row in 0 until height) {\n            var tr = 0\n            var tg = 0\n            var tb = 0\n            for (col in 0 until width) {\n                index = row * width + col\n\n                tr = R[index].toInt() and 0xff\n                tg = G[index].toInt() and 0xff\n                tb = B[index].toInt() and 0xff\n\n                // remove means\n                tr -= rgbmeans[0]\n                tg -= rgbmeans[1]\n                tb -= rgbmeans[2]\n\n                tr *= contrast.toInt()\n                tg *= contrast.toInt()\n                tb *= contrast.toInt()\n\n                tr += rgbmeans[0] * brightness.toInt()\n                tg += rgbmeans[1] * brightness.toInt()\n                tb += rgbmeans[2] * brightness.toInt()\n\n                R[index] = clamp(tr).toByte()\n                G[index] = clamp(tg).toByte()\n                B[index] = clamp(tb).toByte()\n            }\n        }\n\n        return toBufferedImage(dstImage)\n    }\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/filter/CropFilter.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.filter\n\nimport cn.netdiscovery.monica.imageprocess.filter.base.BaseFilter\nimport java.awt.Graphics2D\nimport java.awt.geom.AffineTransform\nimport java.awt.image.BufferedImage\n\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.filter.CropFilter\n * @author: Tony Shen\n * @date: 2024/5/5 13:14\n * @version: V1.0 <描述当前版本功能>\n */\nclass CropFilter(private val x:Int = 0,\n                 private val y:Int = 0,\n                 private val w:Int = 32,\n                 private val h:Int = 32): BaseFilter() {\n\n    override fun doFilter(srcImage: BufferedImage, dstImage: BufferedImage): BufferedImage {\n\n        val dst = BufferedImage(w, h, type)\n        val g: Graphics2D = dst.createGraphics()\n        g.drawRenderedImage(srcImage, AffineTransform.getTranslateInstance(-x.toDouble(), -y.toDouble()))\n        g.dispose()\n        return dst\n    }\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/filter/CrystallizeFilter.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.filter\n\nimport cn.netdiscovery.monica.imageprocess.math.mixColors\nimport cn.netdiscovery.monica.imageprocess.math.smoothStep\nimport cn.netdiscovery.monica.imageprocess.utils.clamp\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.filter.CrystallizeFilter\n * @author: Tony Shen\n * @date:  2025/3/22 16:50\n * @version: V1.0 <描述当前版本功能>\n */\nclass CrystallizeFilter(private val edgeThickness:Float = 0.4f,\n                        override var scale:Float = 16f,\n                        override var randomness:Float = 0f,\n                        override var gridType:Int = HEXAGONAL) : CellularFilter(scale = scale, randomness = randomness, gridType = gridType) {\n\n    private var fadeEdges = false\n    private var edgeColor = 0xff000000.toInt()\n\n    override fun getPixel(x: Int, y: Int, inPixels: IntArray, width: Int, height: Int): Int {\n        var nx: Float = m00 * x + m01 * y\n        var ny: Float = m10 * x + m11 * y\n        nx /= scale\n        ny /= scale * stretch\n        nx += 1000f\n        ny += 1000f // Reduce artifacts around 0,0\n        var f: Float = evaluate(nx, ny)\n\n        val f1: Float = results[0]!!.distance\n        val f2: Float = results[1]!!.distance\n        var srcx: Int = clamp(((results[0]!!.x - 1000) * scale).toInt(), 0, width - 1)\n        var srcy: Int = clamp(((results[0]!!.y - 1000) * scale).toInt(), 0, height - 1)\n        var v = inPixels[srcy * width + srcx]\n        f = (f2 - f1) / edgeThickness\n        f = smoothStep(0f, edgeThickness, f)\n        if (fadeEdges) {\n            srcx = clamp(((results[1]!!.x - 1000) * scale).toInt(), 0, width - 1)\n            srcy = clamp(((results[1]!!.y - 1000) * scale).toInt(), 0, height - 1)\n            var v2 = inPixels[srcy * width + srcx]\n            v2 = mixColors(0.5f, v2, v)\n            v = mixColors(f, v2, v)\n        } else v = mixColors(f, edgeColor, v)\n        return v\n    }\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/filter/DiffuseFilter.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.filter\n\nimport cn.netdiscovery.monica.imageprocess.filter.base.TransformFilter\nimport cn.netdiscovery.monica.imageprocess.math.TWO_PI\nimport kotlin.math.cos\nimport kotlin.math.sin\n\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.filter.DiffuseFilter\n * @author: Tony Shen\n * @date:  2025/3/8 16:05\n * @version: V1.0 <描述当前版本功能>\n */\nclass DiffuseFilter(private val scale: Float = 4f): TransformFilter() {\n\n    private lateinit var sinTable: FloatArray\n    private lateinit var cosTable: FloatArray\n\n    init {\n        edgeAction = CLAMP\n        initialize()\n    }\n\n    private fun initialize() {\n        sinTable = FloatArray(256)\n        cosTable = FloatArray(256)\n        for (i in 0..255) {\n            val angle: Float = TWO_PI * i / 256f\n            sinTable[i] = (scale * sin(angle.toDouble())).toFloat()\n            cosTable[i]= (scale * cos(angle.toDouble())).toFloat()\n        }\n    }\n\n    override fun transformInverse(x: Int, y: Int, out: FloatArray) {\n        val angle = (Math.random() * 255).toInt()\n        val distance = Math.random().toFloat()\n        out[0] = x + distance * sinTable[angle]\n        out[1] = y + distance * cosTable.get(angle)\n    }\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/filter/EmbossFilter.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.filter\n\nimport cn.netdiscovery.monica.imageprocess.filter.base.ColorProcessorFilter\nimport cn.netdiscovery.monica.imageprocess.utils.clamp\nimport java.awt.image.BufferedImage\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.filter.EmbossFilter\n * @author: Tony Shen\n * @date: 2024/5/9 11:01\n * @version: V1.0 <描述当前版本功能>\n */\nclass EmbossFilter(private val colorConstant:Int = 100, private val out:Boolean = false): ColorProcessorFilter() {\n\n    override fun doColorProcessor(dstImage: BufferedImage): BufferedImage {\n        var offset = 0\n        var r1 = 0\n        var g1 = 0\n        var b1 = 0\n        var r2 = 0\n        var g2 = 0\n        var b2 = 0\n        var r = 0\n        var g = 0\n        var b = 0\n\n        for (y in 1 until height - 1) {\n            offset = y * width\n            var ta = 0\n            for (x in 1 until width - 1) {\n                r1 = R[offset].toInt() and 0xff\n                g1 = G[offset].toInt() and 0xff\n                b1 = B[offset].toInt() and 0xff\n                r2 = R[offset + width].toInt() and 0xff\n                g2 = G[offset + width].toInt() and 0xff\n                b2 = B[offset + width].toInt() and 0xff\n                if (out) {\n                    r = r1 - r2\n                    g = g1 - g2\n                    b = b1 - b2\n                } else {\n                    r = r2 - r1\n                    g = g2 - g1\n                    b = b2 - b1\n                }\n\n                R[offset] = clamp(r + colorConstant).toByte()\n                G[offset] = clamp(g + colorConstant).toByte()\n                B[offset] = clamp(b + colorConstant).toByte()\n\n                offset++\n            }\n        }\n\n        return toBufferedImage(dstImage)\n    }\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/filter/EqualizeFilter.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.filter\n\nimport cn.netdiscovery.monica.imageprocess.domain.Histogram\nimport cn.netdiscovery.monica.imageprocess.filter.base.WholeImageFilter\nimport java.awt.Rectangle\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.filter.EqualizeFilter\n * @author: Tony Shen\n * @date: 2025/3/20 19:58\n * @version: V1.0 <描述当前版本功能>\n */\nclass EqualizeFilter: WholeImageFilter() {\n\n    private var lut: Array<IntArray>?=null\n\n    override fun filterPixels(width: Int, height: Int, inPixels: IntArray, transformedSpace: Rectangle): IntArray {\n        val histogram = Histogram(inPixels, width, height, 0, width)\n\n        var i: Int\n        var j: Int\n\n        if (histogram.getNumSamples() > 0) {\n            val scale: Float = 255.0f / histogram.getNumSamples()\n            lut = Array(3) { IntArray(256) }\n            i = 0\n            while (i < 3) {\n                lut!![i][0] = histogram.getFrequency(i, 0)\n                j = 1\n                while (j < 256) {\n                    lut!![i][j] = lut!![i][j - 1] + histogram.getFrequency(i, j)\n                    j++\n                }\n                j = 0\n                while (j < 256) {\n                    lut!![i][j] = Math.round(lut!![i][j] * scale)\n                    j++\n                }\n                i++\n            }\n        } else lut = null\n\n        i = 0\n        for (y in 0..<height) {\n            for (x in 0..<width) {\n                inPixels[i] = filterRGB(x, y, inPixels[i])\n                i++\n            }\n        }\n\n        lut = null\n\n        return inPixels\n    }\n\n\n    private fun filterRGB(x: Int, y: Int, rgb: Int): Int {\n        if (lut != null) {\n            val a = rgb and 0xff000000.toInt()\n            val r = lut!![Histogram.RED][(rgb shr 16) and 0xff]\n            val g = lut!![Histogram.GREEN][(rgb shr 8) and 0xff]\n            val b = lut!![Histogram.BLUE][rgb and 0xff]\n\n            return a or (r shl 16) or (g shl 8) or b\n        }\n        return rgb\n    }\n\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/filter/ExposureFilter.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.filter\n\nimport cn.netdiscovery.monica.imageprocess.filter.base.TransferFilter\nimport kotlin.math.exp\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.filter.ExposureFilter\n * @author: Tony Shen\n * @date: 2025/3/12 18:10\n * @version: V1.0 <描述当前版本功能>\n */\nclass ExposureFilter(private val exposure:Float = 1f): TransferFilter(){\n\n    override fun transferFunction(f: Float): Float {\n        return 1 - exp((-f * exposure).toDouble()).toFloat()\n    }\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/filter/GainFilter.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.filter\n\nimport cn.netdiscovery.monica.imageprocess.filter.base.TransferFilter\nimport cn.netdiscovery.monica.imageprocess.math.bias\nimport cn.netdiscovery.monica.imageprocess.math.gain\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.filter.GainFilter\n * @author: Tony Shen\n * @date: 2025/3/13 10:52\n * @version: V1.0 <描述当前版本功能>\n */\nclass GainFilter(private val gain:Float = 0.5f, private val bias:Float = 0.5f): TransferFilter() {\n\n    override fun transferFunction(v: Float): Float {\n        var f = v\n        f = gain(f, gain)\n        f = bias(f, bias)\n        return f\n    }\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/filter/GammaFilter.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.filter\n\nimport cn.netdiscovery.monica.imageprocess.filter.base.BaseFilter\nimport cn.netdiscovery.monica.imageprocess.utils.clamp\nimport java.awt.image.BufferedImage\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.filter.GammaFilter\n * @author: Tony Shen\n * @date: 2024/4/29 17:34\n * @version: V1.0 <描述当前版本功能>\n */\nclass GammaFilter(private val gamma:Double = 0.5): BaseFilter() {\n\n    private val lut: IntArray = IntArray(256)\n\n    init {\n        setupGammaLut()\n    }\n\n    private fun setupGammaLut() {\n        for (i in 0..255) {\n            lut[i] = (Math.exp(Math.log(i / 255.0) * gamma) * 255.0).toInt()\n        }\n    }\n\n    override fun doFilter(srcImage: BufferedImage, dstImage: BufferedImage): BufferedImage {\n\n        val outPixels = IntArray(width * height)\n\n        var index = 0\n        for (row in 0 until height) {\n            var ta = 0\n            var tr = 0\n            var tg = 0\n            var tb = 0\n            for (col in 0 until width) {\n                index = row * width + col\n                ta = inPixels[index] shr 24 and 0xff\n                tr = inPixels[index] shr 16 and 0xff\n                tg = inPixels[index] shr 8 and 0xff\n                tb = inPixels[index] and 0xff\n\n                // LUT search\n                tr = lut[tr]\n                tg = lut[tg]\n                tb = lut[tb]\n                outPixels[index] = ta shl 24 or (clamp(tr) shl 16) or (clamp(tg) shl 8) or clamp(tb)\n            }\n        }\n\n        setRGB(dstImage, 0, 0, width, height, outPixels)\n        return dstImage\n    }\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/filter/GaussianNoiseFilter.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.filter\n\nimport cn.netdiscovery.monica.imageprocess.filter.base.ColorProcessorFilter\nimport cn.netdiscovery.monica.imageprocess.utils.clamp\nimport java.awt.image.BufferedImage\nimport java.util.*\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.filter.GaussianNoiseFilter\n * @author: Tony Shen\n * @date: 2025/3/17 12:18\n * @version: V1.0 <描述当前版本功能>\n */\nclass GaussianNoiseFilter(private val sigma:Int = 25): ColorProcessorFilter() {\n\n    override fun doColorProcessor(dstImage: BufferedImage): BufferedImage {\n\n        var r = 0\n        var g = 0\n        var b = 0\n\n        val total = width * height\n        val random = Random()\n        for (i in 0..<total) {\n            r = R[i].toInt() and 0xff\n            g = G[i].toInt() and 0xff\n            b = B[i].toInt() and 0xff\n\n            // add Gaussian noise\n            r = (r + sigma * random.nextGaussian()).toInt()\n            g = (g + sigma * random.nextGaussian()).toInt()\n            b = (b + sigma * random.nextGaussian()).toInt()\n\n            R[i] = clamp(r).toByte()\n            G[i] = clamp(g).toByte()\n            B[i] = clamp(b).toByte()\n        }\n\n        return toBufferedImage(dstImage)\n    }\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/filter/GradientFilter.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.filter\n\nimport cn.netdiscovery.monica.imageprocess.filter.base.BaseFilter\nimport cn.netdiscovery.monica.imageprocess.utils.clamp\nimport java.awt.image.BufferedImage\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.filter.GradientFilter\n * @author: Tony Shen\n * @date: 2024/5/1 10:24\n * @version: V1.0 <描述当前版本功能>\n */\n// prewitt operator\nval PREWITT_X = arrayOf(intArrayOf(-1, 0, 1), intArrayOf(-1, 0, 1), intArrayOf(-1, 0, 1))\nval PREWITT_Y = arrayOf(intArrayOf(-1, -1, -1), intArrayOf(0, 0, 0), intArrayOf(1, 1, 1))\n\n// sobel operator\nval SOBEL_X = arrayOf(intArrayOf(-1, 0, 1), intArrayOf(-2, 0, 2), intArrayOf(-1, 0, 1))\nval SOBEL_Y = arrayOf(intArrayOf(-1, -2, -1), intArrayOf(0, 0, 0), intArrayOf(1, 2, 1))\n\n// direction parameter\nval X_DIRECTION = 0\nval Y_DIRECTION = 2\nval XY_DIRECTION = 4\n\nclass GradientFilter(val direction: Int = XY_DIRECTION, val isSobel:Boolean = true): BaseFilter() {\n\n    override fun doFilter(srcImage: BufferedImage, dstImage: BufferedImage): BufferedImage {\n\n        val outPixels = IntArray(width * height)\n\n        var index = 0\n        var index2 = 0\n        var xred = 0.0\n        var xgreen = 0.0\n        var xblue = 0.0\n        var yred = 0.0\n        var ygreen = 0.0\n        var yblue = 0.0\n        var newRow: Int\n        var newCol: Int\n        for (row in 0 until height) {\n            val ta = 255\n            var tr = 0\n            var tg = 0\n            var tb = 0\n            for (col in 0 until width) {\n                index = row * width + col\n                for (subrow in -1..1) {\n                    for (subcol in -1..1) {\n                        newRow = row + subrow\n                        newCol = col + subcol\n                        if (newRow < 0 || newRow >= height) {\n                            newRow = row\n                        }\n                        if (newCol < 0 || newCol >= width) {\n                            newCol = col\n                        }\n                        index2 = newRow * width + newCol\n                        tr = inPixels[index2] shr 16 and 0xff\n                        tg = inPixels[index2] shr 8 and 0xff\n                        tb = inPixels[index2] and 0xff\n                        if (isSobel) {\n                            xred += SOBEL_X[subrow + 1][subcol + 1] * tr\n                            xgreen += SOBEL_X[subrow + 1][subcol + 1] * tg\n                            xblue += SOBEL_X[subrow + 1][subcol + 1] * tb\n                            yred += SOBEL_Y[subrow + 1][subcol + 1] * tr\n                            ygreen += SOBEL_Y[subrow + 1][subcol + 1] * tg\n                            yblue += SOBEL_Y[subrow + 1][subcol + 1] * tb\n                        } else {\n                            xred += PREWITT_X[subrow + 1][subcol + 1] * tr\n                            xgreen += PREWITT_X[subrow + 1][subcol + 1] * tg\n                            xblue += PREWITT_X[subrow + 1][subcol + 1] * tb\n                            yred += PREWITT_Y[subrow + 1][subcol + 1] * tr\n                            ygreen += PREWITT_Y[subrow + 1][subcol + 1] * tg\n                            yblue += PREWITT_Y[subrow + 1][subcol + 1] * tb\n                        }\n                    }\n                }\n                val mred = Math.sqrt(xred * xred + yred * yred)\n                val mgreen = Math.sqrt(xgreen * xgreen + ygreen * ygreen)\n                val mblue = Math.sqrt(xblue * xblue + yblue * yblue)\n                if (XY_DIRECTION === direction) {\n                    outPixels[index] =\n                        ta shl 24 or (clamp(mred.toInt()) shl 16) or (clamp(mgreen.toInt()) shl 8) or clamp(mblue.toInt())\n                } else if (X_DIRECTION === direction) {\n                    outPixels[index] =\n                        ta shl 24 or (clamp(yred.toInt()) shl 16) or (clamp(ygreen.toInt()) shl 8) or clamp(yblue.toInt())\n                } else if (Y_DIRECTION === direction) {\n                    outPixels[index] =\n                        ta shl 24 or (clamp(xred.toInt()) shl 16) or (clamp(xgreen.toInt()) shl 8) or clamp(xblue.toInt())\n                } else {\n                    // as default, always XY gradient\n                    outPixels[index] =\n                        ta shl 24 or (clamp(mred.toInt()) shl 16) or (clamp(mgreen.toInt()) shl 8) or clamp(mblue.toInt())\n                }\n\n                // cleanup for next loop\n                newCol = 0\n                newRow = newCol\n                xblue = 0.0\n                xgreen = xblue\n                xred = xgreen\n                yblue = 0.0\n                ygreen = yblue\n                yred = ygreen\n            }\n        }\n\n        setRGB(dstImage, 0, 0, width, height, outPixels)\n        return dstImage\n    }\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/filter/GrayFilter.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.filter\n\nimport cn.netdiscovery.monica.imageprocess.filter.base.BaseFilter\nimport java.awt.Color\nimport java.awt.image.BufferedImage\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.filter.GrayFilter\n * @author: Tony Shen\n * @date: 2024/5/1 10:44\n * @version: V1.0 <描述当前版本功能>\n */\nclass GrayFilter: BaseFilter() {\n\n    override fun doFilter(srcImage: BufferedImage, dstImage: BufferedImage): BufferedImage {\n\n        for (row in 0 until height) {\n            for (col in 0 until width) {\n                val rgb = srcImage.getRGB(col,row)\n                val r = rgb and (0x00ff0000 shr 16)\n                val g = rgb and (0x0000ff00 shr 8)\n                val b = rgb and 0x000000ff\n\n                val color = (r * 0.299 + g * 0.587 + b * 0.114).toInt()\n                dstImage.setRGB(col, row, Color(color, color, color).rgb)\n            }\n        }\n\n        return dstImage\n    }\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/filter/HSBAdjustFilter.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.filter\n\nimport cn.netdiscovery.monica.imageprocess.filter.base.PointFilter\nimport cn.netdiscovery.monica.imageprocess.math.PI\nimport java.awt.Color\n\n\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.filter.HSBAdjustFilter\n * @author: Tony Shen\n * @date: 2025/3/24 11:29\n * @version: V1.0 <描述当前版本功能>\n */\nclass HSBAdjustFilter(private val hFactor:Float = 0f, private val sFactor:Float = 0f, private val bFactor:Float = 0f): PointFilter() {\n\n    private val hsb = FloatArray(3)\n\n    init {\n        canFilterIndexColorModel = true\n    }\n\n    override fun filterRGB(x: Int, y: Int, rgb: Int): Int {\n        val a = rgb and 0xff000000.toInt()\n        val r = (rgb shr 16) and 0xff\n        val g = (rgb shr 8) and 0xff\n        val b = rgb and 0xff\n        Color.RGBtoHSB(r, g, b, hsb)\n        hsb[0] += hFactor\n        while (hsb[0] < 0)\n                hsb[0] += PI * 2\n        hsb[1] += sFactor\n        if (hsb[1] < 0)\n            hsb[1] = 0f\n        else if (hsb[1] > 1.0)\n            hsb[1] = 1.0f\n        hsb[2] += bFactor\n\n        if (hsb[2] < 0)\n            hsb[2] = 0f\n        else if (hsb[2] > 1.0)\n            hsb[2] = 1.0f\n\n        return a or (Color.HSBtoRGB(hsb[0], hsb[1], hsb[2]) and 0xffffff)\n    }\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/filter/HighPassFilter.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.filter\n\nimport cn.netdiscovery.monica.imageprocess.filter.blur.GaussianFilter\nimport java.awt.image.BufferedImage\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.filter.HighPassFilter\n * @author: Tony Shen\n * @date: 2024/5/5 14:00\n * @version: V1.0 <描述当前版本功能>\n */\nclass HighPassFilter(override val radius: Float =10f): GaussianFilter(radius) {\n\n    override fun doFilter(srcImage: BufferedImage, dstImage: BufferedImage): BufferedImage {\n\n        val outPixels = IntArray(width * height)\n\n        if (radius > 0) {\n            convolveAndTranspose(kernel, inPixels, outPixels, width, height, alpha, alpha && premultiplyAlpha, false, CLAMP_EDGES)\n            convolveAndTranspose(kernel, outPixels, inPixels, height, width, alpha, false, alpha && premultiplyAlpha, CLAMP_EDGES)\n        }\n\n        getRGB(srcImage, 0, 0, width, height, outPixels)\n\n        var index = 0\n        for (y in 0 until height) {\n            for (x in 0 until width) {\n                val rgb1 = outPixels[index]\n                var r1 = rgb1 shr 16 and 0xff\n                var g1 = rgb1 shr 8 and 0xff\n                var b1 = rgb1 and 0xff\n                val rgb2 = inPixels[index]\n                val r2 = rgb2 shr 16 and 0xff\n                val g2 = rgb2 shr 8 and 0xff\n                val b2 = rgb2 and 0xff\n                r1 = (r1 + 255 - r2) / 2\n                g1 = (g1 + 255 - g2) / 2\n                b1 = (b1 + 255 - b2) / 2\n                inPixels[index] = (rgb1 and 0xff000000.toInt()) or (r1 shl 16) or (g1 shl 8) or b1\n                index++\n            }\n        }\n\n        setRGB(dstImage, 0, 0, width, height, inPixels)\n        return dstImage\n    }\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/filter/InvertFilter.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.filter\n\nimport cn.netdiscovery.monica.imageprocess.filter.base.PointFilter\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.filter.InvertFilter\n * @author: Tony Shen\n * @date: 2025/3/11 21:16\n * @version: V1.0 <描述当前版本功能>\n */\nclass InvertFilter:PointFilter() {\n\n    init {\n        canFilterIndexColorModel = true\n    }\n\n    override fun filterRGB(x: Int, y: Int, rgb: Int): Int {\n        val a = rgb and 0xff000000.toInt()\n        return a or (rgb.inv() and 0x00ffffff)\n    }\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/filter/MarbleFilter.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.filter\n\nimport cn.netdiscovery.monica.imageprocess.filter.base.TransformFilter\nimport cn.netdiscovery.monica.imageprocess.math.Noise\nimport cn.netdiscovery.monica.imageprocess.math.TWO_PI\nimport cn.netdiscovery.monica.imageprocess.utils.clamp\nimport kotlin.math.cos\nimport kotlin.math.sin\n\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.filter.MarbleFilter\n * @author: Tony Shen\n * @date: 2025/3/11 14:35\n * @version: V1.0 <描述当前版本功能>\n */\nclass MarbleFilter(private val xScale:Float = 4f, private val yScale:Float = 4f, private val turbulence:Float = 1f): TransformFilter() {\n\n    private lateinit var sinTable: FloatArray\n    private lateinit var cosTable: FloatArray\n\n    init {\n        edgeAction = CLAMP\n        initialize()\n    }\n\n    private fun initialize() {\n        sinTable = FloatArray(256)\n        cosTable = FloatArray(256)\n        for (i in 0..255) {\n            val angle: Float = TWO_PI * i / 256f * turbulence\n            sinTable[i] = (-yScale * sin(angle.toDouble())).toFloat()\n            cosTable[i] = (yScale * cos(angle.toDouble())).toFloat()\n        }\n    }\n\n    override fun transformInverse(x: Int, y: Int, out: FloatArray) {\n        val displacement: Int = displacementMap(x, y)\n        out[0] = x + sinTable[displacement]\n        out[1] = y + cosTable[displacement]\n    }\n\n    private fun displacementMap(x: Int, y: Int): Int {\n        return clamp((127 * (1 + Noise.noise2(x / xScale, y / xScale))).toInt())\n    }\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/filter/MirrorFilter.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.filter\n\nimport cn.netdiscovery.monica.imageprocess.filter.base.BaseFilter\nimport java.awt.*\nimport java.awt.image.BufferedImage\n\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.filter.MirrorFilter\n * @author: Tony Shen\n * @date: 2025/3/18 20:25\n * @version: V1.0 <描述当前版本功能>\n */\nclass MirrorFilter(private val opacity:Float = 1.0f, private val centreY:Float = 0.5f, private val gap:Float = 0f): BaseFilter() {\n\n    override fun doFilter(srcImage: BufferedImage, dstImage: BufferedImage): BufferedImage {\n\n        val clip: Shape\n        val h = (centreY * height).toInt()\n        val d = (gap * height).toInt()\n\n        val g: Graphics2D = dstImage.createGraphics()\n        clip = g.clip\n        g.clipRect(0, 0, width, h)\n        g.drawRenderedImage(srcImage, null)\n        g.clip = clip\n        g.clipRect(0, h + d, width, height - h - d)\n        g.translate(0, 2 * h + d)\n        g.scale(1.0, -1.0)\n        g.drawRenderedImage(srcImage, null)\n        g.paint =\n            GradientPaint(0f, 0f, Color(1.0f, 0.0f, 0.0f, 0.0f), 0f, h.toFloat(), Color(0.0f, 1.0f, 0.0f, opacity))\n        g.composite = AlphaComposite.getInstance(AlphaComposite.DST_IN)\n        g.fillRect(0, 0, width, h)\n        g.clip = clip\n        g.dispose()\n\n\n        return dstImage\n    }\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/filter/MosaicFilter.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.filter\n\nimport cn.netdiscovery.monica.imageprocess.IntIntegralImage\nimport cn.netdiscovery.monica.imageprocess.filter.base.ColorProcessorFilter\nimport java.awt.image.BufferedImage\n\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.filter.MosaicFilter\n * @author: Tony Shen\n * @date:  2024/7/6 14:51\n * @version: V1.0 <描述当前版本功能>\n */\nclass MosaicFilter(val r:Int=3): ColorProcessorFilter() {\n\n    override fun doColorProcessor(dstImage: BufferedImage): BufferedImage {\n        val size = (r * 2 + 1) * (r * 2 + 1)\n        var tr = 0\n        var tg = 0\n        var tb = 0\n        var output:Array<ByteArray>? = Array(3) { ByteArray(R.size) }\n\n        val rii = IntIntegralImage()\n        rii.setImage(R)\n        rii.calculate(width, height)\n\n        val gii = IntIntegralImage()\n        gii.setImage(G)\n        gii.calculate(width, height)\n\n        val bii = IntIntegralImage()\n        bii.setImage(B)\n        bii.calculate(width, height)\n\n        var x2 = 0\n        var y2 = 0\n        var x1 = 0\n        var y1 = 0\n        var index = 0\n        for (row in 0 until height) {\n            val dy = (row / size)\n            y1 = dy * size\n            y2 = if ((y1 + size) > height) (height - 1) else (y1 + size)\n            index = row * width\n            for (col in 0 until width) {\n                val dx = (col / size)\n                x1 = dx * size\n                x2 = if ((x1 + size) > width) (width - 1) else (x1 + size)\n                val sr = rii.getBlockSum(x1, y1, x2, y2)\n                val sg = gii.getBlockSum(x1, y1, x2, y2)\n                val sb = bii.getBlockSum(x1, y1, x2, y2)\n                val num = (x2 - x1) * (y2 - y1)\n                tr = sr / num\n                tg = sg / num\n                tb = sb / num\n                output!![0][index + col] = tr.toByte()\n                output[1][index + col] = tg.toByte()\n                output[2][index + col] = tb.toByte()\n            }\n        }\n\n        setRGB(dstImage, width,height, inPixels, output?.get(0)!!, output[1], output[2])\n\n        output = null\n\n        return dstImage\n    }\n\n}\n"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/filter/NatureFilter.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.filter\n\nimport cn.netdiscovery.monica.imageprocess.filter.base.ColorProcessorFilter\nimport cn.netdiscovery.monica.imageprocess.utils.clamp\nimport java.awt.image.BufferedImage\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.filter.NatureFilter\n * @author: Tony Shen\n * @date: 2024/7/11 14:06\n * @version: V1.0 <描述当前版本功能>\n */\nclass NatureFilter(val style:Int = 0) : ColorProcessorFilter() {\n\n    val ATMOSPHERE_STYLE = 1\n    val BURN_STYLE = 2\n    val FOG_STYLE = 3\n    val FREEZE_STYLE = 4\n    val LAVA_STYLE = 5\n    val METAL_STYLE = 6\n    val OCEAN_STYLE = 7\n    val WATER_STYLE = 8\n\n    private lateinit var fogLookUp: IntArray\n\n    init {\n        buildFogLookupTable()\n    }\n\n    private fun buildFogLookupTable() {\n        fogLookUp = IntArray(256)\n        val fogLimit = 40\n        for (i in fogLookUp.indices) {\n            if (i > 127) {\n                fogLookUp[i] = i - fogLimit\n                if (fogLookUp[i] < 127) {\n                    fogLookUp[i] = 127\n                }\n            } else {\n                fogLookUp[i] = i + fogLimit\n                if (fogLookUp[i] > 127) {\n                    fogLookUp[i] = 127\n                }\n            }\n        }\n    }\n\n    override fun doColorProcessor(dstImage: BufferedImage): BufferedImage {\n        val ta = 0\n        var tr = 0\n        var tg = 0\n        var tb = 0\n        for (i in 0 until size) {\n            tr = R[i].toInt() and 0xff\n            tg = G[i].toInt()  and 0xff\n            tb = B[i].toInt()  and 0xff\n            val onePixel: IntArray = processOnePixel(ta, tr, tg, tb)\n            R[i] = onePixel[0].toByte()\n            G[i] = onePixel[1].toByte()\n            B[i] = onePixel[2].toByte()\n        }\n\n        return toBufferedImage(dstImage)\n    }\n\n    private fun processOnePixel(ta: Int, tr: Int, tg: Int, tb: Int): IntArray {\n        val pixel = IntArray(4)\n        pixel[0] = ta\n        val gray = (tr + tg + tb) / 3\n        when (style) {\n            ATMOSPHERE_STYLE -> {\n                pixel[1] = (tg + tb) / 2\n                pixel[2] = (tr + tb) / 2\n                pixel[3] = (tg + tr) / 2\n            }\n\n            BURN_STYLE -> {\n                pixel[1] = clamp(gray * 3)\n                pixel[2] = gray\n                pixel[3] = gray / 3\n            }\n\n            FOG_STYLE -> {\n                pixel[1] = fogLookUp[tr]\n                pixel[2] = fogLookUp[tg]\n                pixel[3] = fogLookUp[tb]\n            }\n\n            FREEZE_STYLE -> {\n                pixel[1] = clamp(Math.abs((tr - tg - tb) * 1.5).toInt())\n                pixel[2] = clamp(Math.abs((tg - tb - pixel[1]) * 1.5).toInt())\n                pixel[3] = clamp(Math.abs((tb - pixel[1] - pixel[2]) * 1.5).toInt())\n            }\n\n            LAVA_STYLE -> {\n                pixel[1] = gray\n                pixel[2] = Math.abs(tb - 128)\n                pixel[3] = Math.abs(tb - 128)\n            }\n\n            METAL_STYLE -> {\n                var r = Math.abs(tr - 64).toFloat()\n                var g = Math.abs(r - 64)\n                var b = Math.abs(g - 64)\n                val grayFloat = (222 * r + 707 * g + 71 * b) / 1000\n                r = grayFloat + 70\n                r = r + (r - 128) * 100 / 100f\n                g = grayFloat + 65\n                g = g + (g - 128) * 100 / 100f\n                b = grayFloat + 75\n                b = b + (b - 128) * 100 / 100f\n                pixel[1] = clamp(r.toInt())\n                pixel[2] = clamp(g.toInt())\n                pixel[3] = clamp(b.toInt())\n            }\n\n            OCEAN_STYLE -> {\n                pixel[1] = clamp(gray / 3)\n                pixel[2] = gray\n                pixel[3] = clamp(gray * 3)\n            }\n\n            WATER_STYLE -> {\n                pixel[1] = clamp(gray - tg - tb)\n                pixel[2] = clamp(gray - pixel[1] - tb)\n                pixel[3] = clamp(gray - pixel[1] - pixel[2])\n            }\n\n            else -> {\n                pixel[1] = (tg + tb) / 2\n                pixel[2] = (tr + tb) / 2\n                pixel[3] = (tg + tr) / 2\n            }\n        }\n        return pixel\n    }\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/filter/OffsetFilter.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.filter\n\nimport cn.netdiscovery.monica.imageprocess.filter.base.TransformFilter\nimport java.awt.image.BufferedImage\n\n\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.filter.OffsetFilter\n * @author: Tony Shen\n * @date: 2025/3/24 09:53\n * @version: V1.0 <描述当前版本功能>\n */\nclass OffsetFilter(private var xOffset:Int = 0, private var yOffset:Int = 0, private val wrap:Boolean = true): TransformFilter() {\n\n    init {\n        edgeAction = ZERO\n    }\n\n    override fun transformInverse(x: Int, y: Int, out: FloatArray) {\n        if ( wrap ) {\n            out[0] = ((x+width-xOffset) % width).toFloat()\n            out[1] = ((y+height-yOffset) % height).toFloat()\n        } else {\n            out[0] = (x-xOffset).toFloat()\n            out[1] = (y-yOffset).toFloat()\n        }\n    }\n\n    override fun doFilter(srcImage: BufferedImage, dstImage: BufferedImage): BufferedImage {\n        if (wrap) {\n            while (xOffset < 0) xOffset += width\n            while (yOffset < 0) yOffset += height\n            xOffset %= width\n            yOffset %= height\n        }\n        return super.doFilter(srcImage, dstImage)\n    }\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/filter/OilPaintFilter.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.filter\n\nimport cn.netdiscovery.monica.imageprocess.filter.base.BaseFilter\nimport java.awt.image.BufferedImage\n\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.filter.OilPaintFilter\n * @author: Tony Shen\n * @date: 2024/5/8 20:38\n * @version: V1.0 <描述当前版本功能>\n */\nclass OilPaintFilter(private val ksize:Int = 10,private val intensity:Int = 40): BaseFilter() {\n\n    override fun doFilter(srcImage: BufferedImage, dstImage: BufferedImage): BufferedImage {\n\n        val outPixels = IntArray(size)\n\n        var index = 0\n        val subradius: Int = this.ksize / 2\n        val intensityCount = IntArray(intensity + 1)\n        val ravg = IntArray(intensity + 1)\n        val gavg = IntArray(intensity + 1)\n        val bavg = IntArray(intensity + 1)\n        for (i in 0..intensity) {\n            intensityCount[i] = 0\n            ravg[i] = 0\n            gavg[i] = 0\n            bavg[i] = 0\n        }\n        for (row in 0 until height) {\n            val ta = 0\n            var tr = 0\n            var tg = 0\n            var tb = 0\n            for (col in 0 until width) {\n                for (subRow in -subradius..subradius) {\n                    for (subCol in -subradius..subradius) {\n                        var nrow = row + subRow\n                        var ncol = col + subCol\n                        if (nrow >= height || nrow < 0) {\n                            nrow = 0\n                        }\n                        if (ncol >= width || ncol < 0) {\n                            ncol = 0\n                        }\n                        index = nrow * width + ncol\n                        tr = inPixels[index] shr 16 and 0xff\n                        tg = inPixels[index] shr 8 and 0xff\n                        tb = inPixels[index] and 0xff\n                        val curIntensity = (((tr + tg + tb) / 3).toDouble() * intensity / 255.0f).toInt()\n                        intensityCount[curIntensity]++\n                        ravg[curIntensity] += tr\n                        gavg[curIntensity] += tg\n                        bavg[curIntensity] += tb\n                    }\n                }\n\n                // find the max number of same gray level pixel\n                var maxCount = 0\n                var maxIndex = 0\n                for (m in intensityCount.indices) {\n                    if (intensityCount[m] > maxCount) {\n                        maxCount = intensityCount[m]\n                        maxIndex = m\n                    }\n                }\n\n                // get average value of the pixel\n                val nr = ravg[maxIndex] / maxCount\n                val ng = gavg[maxIndex] / maxCount\n                val nb = bavg[maxIndex] / maxCount\n                index = row * width + col\n                outPixels[index] = ta shl 24 or (nr shl 16) or (ng shl 8) or nb\n\n                // post clear values for next pixel\n                for (i in 0..intensity) {\n                    intensityCount[i] = 0\n                    ravg[i] = 0\n                    gavg[i] = 0\n                    bavg[i] = 0\n                }\n            }\n        }\n        setRGB(dstImage, 0, 0, width, height, outPixels)\n        return dstImage\n    }\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/filter/PointillizeFilter.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.filter\n\nimport cn.netdiscovery.monica.imageprocess.math.mixColors\nimport cn.netdiscovery.monica.imageprocess.math.smoothStep\nimport cn.netdiscovery.monica.imageprocess.utils.clamp\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.filter.PointillizeFilter\n * @author: Tony Shen\n * @date: 2025/3/23 17:59\n * @version: V1.0 <描述当前版本功能>\n */\nclass PointillizeFilter(private val edgeThickness:Float = 0.4f,\n                        private val fuzziness:Float  = 0.1f,\n                        override var scale:Float = 16f,\n                        override var randomness:Float = 0f,\n                        override var gridType:Int = HEXAGONAL): CellularFilter(scale = scale, randomness = randomness, gridType = gridType) {\n\n    private var fadeEdges = false\n    private var edgeColor = 0xff000000.toInt()\n\n    override fun getPixel(x: Int, y: Int, inPixels: IntArray, width: Int, height: Int): Int {\n        var nx = m00 * x + m01 * y\n        var ny = m10 * x + m11 * y\n        nx /= scale\n        ny /= scale * stretch\n        nx += 1000f\n        ny += 1000f // Reduce artifacts around 0,0\n        var f = evaluate(nx, ny)\n\n        val f1 = results[0]!!.distance\n        var srcx: Int = clamp(((results[0]!!.x - 1000) * scale).toInt(), 0, width - 1)\n        var srcy: Int = clamp(((results[0]!!.y - 1000) * scale).toInt(), 0, height - 1)\n        var v = inPixels[srcy * width + srcx]\n\n        if (fadeEdges) {\n            val f2 = results[1]!!.distance\n            srcx = clamp(((results[1]!!.x - 1000) * scale).toInt(), 0, width - 1)\n            srcy = clamp(((results[1]!!.y - 1000) * scale).toInt(), 0, height - 1)\n            val v2 = inPixels[srcy * width + srcx]\n            v = mixColors(0.5f * f1 / f2, v, v2)\n        } else {\n            f = 1 - smoothStep(edgeThickness, edgeThickness + fuzziness, f1)\n            v = mixColors(f, edgeColor, v)\n        }\n        return v\n    }\n\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/filter/PosterizeFilter.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.filter\n\nimport cn.netdiscovery.monica.imageprocess.filter.base.PointFilter\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.filter.PosterizeFilter\n * @author: Tony Shen\n * @date: 2025/3/12 13:47\n * @version: V1.0 <描述当前版本功能>\n */\nclass PosterizeFilter(private val numLevels:Int = 6): PointFilter() {\n\n    private var levels: IntArray = IntArray(256)\n\n    init{\n        if (numLevels != 1)\n            for (i in 0..255)\n                levels[i] = 255 * (numLevels * i / 256) / (numLevels - 1)\n    }\n\n    override fun filterRGB(x: Int, y: Int, rgb: Int): Int {\n\n        val a = rgb and 0xff000000.toInt()\n        var r = (rgb shr 16) and 0xff\n        var g = (rgb shr 8) and 0xff\n        var b = rgb and 0xff\n        r = levels[r]\n        g = levels[g]\n        b = levels[b]\n        return a or (r shl 16) or (g shl 8) or b\n    }\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/filter/RippleFilter.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.filter\n\nimport cn.netdiscovery.monica.imageprocess.filter.base.TransformFilter\nimport cn.netdiscovery.monica.imageprocess.math.Noise\nimport cn.netdiscovery.monica.imageprocess.math.mod\nimport cn.netdiscovery.monica.imageprocess.math.triangle\nimport java.awt.Rectangle\nimport kotlin.math.sin\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.filter.RippleFilter\n * @author: Tony Shen\n * @date: 2025/3/10 11:06\n * @version: V1.0 <描述当前版本功能>\n */\nclass RippleFilter(private val xAmplitude:Float = 5.0f, private val yAmplitude:Float = 0.0f,\n                   private val xWavelength:Float = 16.0f, private val yWavelength:Float = 16.0f,\n                   private val waveType:Int = 0): TransformFilter() {\n\n    /**\n     * Sine wave ripples.\n     */\n    val SINE: Int = 0\n\n    /**\n     * Sawtooth wave ripples.\n     */\n    val SAWTOOTH: Int = 1\n\n    /**\n     * Triangle wave ripples.\n     */\n    val TRIANGLE: Int = 2\n\n    /**\n     * Noise ripples.\n     */\n    val NOISE: Int = 3\n\n    override fun transformSpace(rect: Rectangle) {\n        if (edgeAction == ZERO) {\n            rect.x -= xAmplitude.toInt()\n            rect.width += (2 * xAmplitude).toInt()\n            rect.y -= yAmplitude.toInt()\n            rect.height += (2 * yAmplitude).toInt()\n        }\n    }\n\n    override fun transformInverse(x: Int, y: Int, out: FloatArray) {\n        val nx = y.toFloat() / xWavelength\n        val ny = x.toFloat() / yWavelength\n        val fx: Float\n        val fy: Float\n        when (waveType) {\n            SINE -> {\n                fx = sin(nx.toDouble()).toFloat()\n                fy = sin(ny.toDouble()).toFloat()\n            }\n\n            SAWTOOTH -> {\n                fx = mod(nx, 1.0f)\n                fy = mod(ny, 1.0f)\n            }\n\n            TRIANGLE -> {\n                fx = triangle(nx)\n                fy = triangle(ny)\n            }\n\n            NOISE -> {\n                fx = Noise.noise1(nx)\n                fy = Noise.noise1(ny)\n            }\n\n            else -> {\n                fx = sin(nx.toDouble()).toFloat()\n                fy = sin(ny.toDouble()).toFloat()\n            }\n        }\n        out[0] = x + xAmplitude * fx\n        out[1] = y + yAmplitude * fy\n    }\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/filter/SepiaToneFilter.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.filter\n\nimport cn.netdiscovery.monica.imageprocess.filter.base.BaseFilter\nimport cn.netdiscovery.monica.imageprocess.utils.clamp\n\nimport java.awt.image.BufferedImage\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.filter.SepiaToneFilter\n * @author: Tony Shen\n * @date: 2024/5/1 11:09\n * @version: V1.0 SepiaTone 滤镜， 老照片特效\n */\nclass SepiaToneFilter : BaseFilter() {\n\n    override fun doFilter(srcImage: BufferedImage, dstImage: BufferedImage): BufferedImage {\n\n        val outPixels = IntArray(width * height)\n\n        var index = 0\n        for (row in 0 until height) {\n            var ta = 0\n            var tr = 0\n            var tg = 0\n            var tb = 0\n            for (col in 0 until width) {\n                index = row * width + col\n                ta = inPixels[index] shr 24 and 0xff\n                tr = inPixels[index] shr 16 and 0xff\n                tg = inPixels[index] shr 8 and 0xff\n                tb = inPixels[index] and 0xff\n\n                val fr = colorBlend(noise(), tr * 0.393 + tg * 0.769 + tb * 0.189, tr).toInt()\n                val fg = colorBlend(noise(), tr * 0.349 + tg * 0.686 + tb * 0.168, tg).toInt()\n                val fb = colorBlend(noise(), tr * 0.272 + tg * 0.534 + tb * 0.131, tb).toInt()\n                outPixels[index] = ta shl 24 or (clamp(fr) shl 16) or (clamp(fg) shl 8) or clamp(fb)\n            }\n        }\n\n        setRGB(dstImage, 0, 0, width, height, outPixels)\n        return dstImage\n    }\n\n    private fun noise(): Double = Math.random() * 0.5 + 0.5\n\n    private fun colorBlend(scale: Double, dest: Double, src: Int): Double {\n        return scale * dest + (1.0 - scale) * src\n    }\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/filter/SmearFilter.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.filter\n\nimport cn.netdiscovery.monica.imageprocess.filter.base.WholeImageFilter\nimport cn.netdiscovery.monica.imageprocess.math.mixColors\nimport java.awt.Rectangle\nimport java.util.*\nimport kotlin.math.abs\nimport kotlin.math.cos\nimport kotlin.math.sin\n\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.filter.SmearFilter\n * @author: Tony Shen\n * @date: 2025/3/21 16:27\n * @version: V1.0 <描述当前版本功能>\n */\nclass SmearFilter(private var angle:Float = 0f,\n        private var density:Float = 0.5f,\n        private var distance:Int = 8,\n        private var shape: Int = CIRCLES,\n        private var mix:Float = 0.5f): WholeImageFilter() {\n\n    companion object {\n        val CROSSES: Int = 0\n        val LINES:   Int = 1\n        val CIRCLES: Int = 2\n        val SQUARES: Int = 3\n    }\n\n    private var seed: Long = 567\n    private var randomGenerator = Random()\n    private var background = false\n\n    override fun filterPixels(width: Int, height: Int, inPixels: IntArray, transformedSpace: Rectangle): IntArray {\n        val outPixels = IntArray(width * height)\n\n        randomGenerator.setSeed(seed)\n        val sinAngle = sin(angle.toDouble()).toFloat()\n        val cosAngle = cos(angle.toDouble()).toFloat()\n\n        var i = 0\n        val numShapes: Int\n\n        for (y in 0..<height) for (x in 0..<width) {\n            outPixels[i] = if (background) -0x1 else inPixels[i]\n            i++\n        }\n\n        when (shape) {\n            CROSSES -> {\n                //Crosses\n                numShapes = (2 * density * width * height / (distance + 1)).toInt()\n                i = 0\n                while (i < numShapes) {\n                    val x = (randomGenerator.nextInt() and 0x7fffffff) % width\n                    val y = (randomGenerator.nextInt() and 0x7fffffff) % height\n                    val length = randomGenerator.nextInt() % distance + 1\n                    val rgb = inPixels[y * width + x]\n                    var x1 = x - length\n                    while (x1 < x + length + 1) {\n                        if (x1 >= 0 && x1 < width) {\n                            val rgb2 = if (background) -0x1 else outPixels[y * width + x1]\n                            outPixels[y * width + x1] = mixColors(mix, rgb2, rgb)\n                        }\n                        x1++\n                    }\n                    var y1 = y - length\n                    while (y1 < y + length + 1) {\n                        if (y1 >= 0 && y1 < height) {\n                            val rgb2 = if (background) -0x1 else outPixels[y1 * width + x]\n                            outPixels[y1 * width + x] = mixColors(mix, rgb2, rgb)\n                        }\n                        y1++\n                    }\n                    i++\n                }\n            }\n\n            LINES -> {\n                numShapes = (2 * density * width * height / 2).toInt()\n\n                i = 0\n                while (i < numShapes) {\n                    val sx = (randomGenerator.nextInt() and 0x7fffffff) % width\n                    val sy = (randomGenerator.nextInt() and 0x7fffffff) % height\n                    val rgb = inPixels[sy * width + sx]\n                    val length = (randomGenerator.nextInt() and 0x7fffffff) % distance\n                    var dx = (length * cosAngle).toInt()\n                    var dy = (length * sinAngle).toInt()\n\n                    val x0 = sx - dx\n                    val y0 = sy - dy\n                    val x1 = sx + dx\n                    val y1 = sy + dy\n                    var d: Int\n                    val incrE: Int\n                    val incrNE: Int\n\n                    val ddx = if (x1 < x0) -1\n                    else 1\n                    val ddy = if (y1 < y0) -1\n                    else 1\n                    dx = x1 - x0\n                    dy = y1 - y0\n                    dx = abs(dx.toDouble()).toInt()\n                    dy = abs(dy.toDouble()).toInt()\n                    var x = x0\n                    var y = y0\n\n                    if (x < width && x >= 0 && y < height && y >= 0) {\n                        val rgb2 = if (background) -0x1 else outPixels[y * width + x]\n                        outPixels[y * width + x] = mixColors(mix, rgb2, rgb)\n                    }\n                    if (abs(dx.toDouble()) > abs(dy.toDouble())) {\n                        d = 2 * dy - dx\n                        incrE = 2 * dy\n                        incrNE = 2 * (dy - dx)\n\n                        while (x != x1) {\n                            if (d <= 0) d += incrE\n                            else {\n                                d += incrNE\n                                y += ddy\n                            }\n                            x += ddx\n                            if (x < width && x >= 0 && y < height && y >= 0) {\n                                val rgb2 = if (background) -0x1 else outPixels[y * width + x]\n                                outPixels[y * width + x] = mixColors(mix, rgb2, rgb)\n                            }\n                        }\n                    } else {\n                        d = 2 * dx - dy\n                        incrE = 2 * dx\n                        incrNE = 2 * (dx - dy)\n\n                        while (y != y1) {\n                            if (d <= 0) d += incrE\n                            else {\n                                d += incrNE\n                                x += ddx\n                            }\n                            y += ddy\n                            if (x < width && x >= 0 && y < height && y >= 0) {\n                                val rgb2 = if (background) -0x1 else outPixels[y * width + x]\n                                outPixels[y * width + x] = mixColors(mix, rgb2, rgb)\n                            }\n                        }\n                    }\n                    i++\n                }\n            }\n\n            SQUARES, CIRCLES -> {\n                val radius = distance + 1\n                val radius2 = radius * radius\n                numShapes = (2 * density * width * height / radius).toInt()\n                i = 0\n                while (i < numShapes) {\n                    val sx = (randomGenerator.nextInt() and 0x7fffffff) % width\n                    val sy = (randomGenerator.nextInt() and 0x7fffffff) % height\n                    val rgb = inPixels[sy * width + sx]\n                    var x = sx - radius\n                    while (x < sx + radius + 1) {\n                        var y = sy - radius\n                        while (y < sy + radius + 1) {\n                            val f = if (shape === CIRCLES) (x - sx) * (x - sx) + (y - sy) * (y - sy)\n                            else 0\n                            if (x >= 0 && x < width && y >= 0 && y < height && f <= radius2) {\n                                val rgb2 = if (background) -0x1 else outPixels[y * width + x]\n                                outPixels[y * width + x] = mixColors(mix, rgb2, rgb)\n                            }\n                            y++\n                        }\n                        x++\n                    }\n                    i++\n                }\n            }\n        }\n\n        return outPixels\n    }\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/filter/SolarizeFilter.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.filter\n\nimport cn.netdiscovery.monica.imageprocess.filter.base.TransferFilter\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.filter.SolarizeFilter\n * @author: Tony Shen\n * @date: 2025/3/19 20:44\n * @version: V1.0 <描述当前版本功能>\n */\nclass SolarizeFilter: TransferFilter() {\n\n    override fun transferFunction(v: Float): Float {\n        return if (v > 0.5f) 2 * (v - 0.5f) else 2 * (0.5f - v)\n    }\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/filter/SpotlightFilter.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.filter\n\nimport cn.netdiscovery.monica.imageprocess.filter.base.BaseFilter\nimport java.awt.image.BufferedImage\nimport kotlin.math.sqrt\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.filter.SpotlightFilter\n * @author: Tony Shen\n * @date: 2024/4/29 15:23\n * @version: V1.0 <描述当前版本功能>\n */\nclass SpotlightFilter(private val factor:Int = 1): BaseFilter() {\n\n    override fun doFilter(srcImage: BufferedImage, dstImage: BufferedImage): BufferedImage {\n\n        val outPixels = IntArray(width * height)\n\n        var index = 0\n        val centerX = width / 2\n        val centerY = height / 2\n        val maxDistance = sqrt((centerX * centerX + centerY * centerY).toDouble())\n        for (row in 0 until height) {\n            var ta = 0\n            var tr = 0\n            var tg = 0\n            var tb = 0\n            for (col in 0 until width) {\n                index = row * width + col\n                ta = inPixels[index] shr 24 and 0xff\n                tr = inPixels[index] shr 16 and 0xff\n                tg = inPixels[index] shr 8 and 0xff\n                tb = inPixels[index] and 0xff\n                var scale: Double = 1.0 - getDistance(centerX, centerY, col, row) / maxDistance\n                for (i in 0 until factor) {\n                    scale = scale * scale\n                }\n                tr = (scale * tr).toInt()\n                tg = (scale * tg).toInt()\n                tb = (scale * tb).toInt()\n                outPixels[index] = ta shl 24 or (tr shl 16) or (tg shl 8) or tb\n            }\n        }\n\n        setRGB(dstImage, 0, 0, width, height, outPixels)\n        return dstImage\n    }\n\n    private fun getDistance(centerX: Int, centerY: Int, px: Int, py: Int): Double {\n        val xx = ((centerX - px) * (centerX - px)).toDouble()\n        val yy = ((centerY - py) * (centerY - py)).toDouble()\n        return sqrt(xx + yy).toInt().toDouble()\n    }\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/filter/StrokeAreaFilter.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.filter\n\nimport cn.netdiscovery.monica.imageprocess.filter.base.BaseFilter\nimport cn.netdiscovery.monica.imageprocess.utils.clamp\nimport java.awt.image.BufferedImage\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.filter.StrokeAreaFilter\n * @author: Tony Shen\n * @date: 2024/5/25 22:00\n * @version: V1.0 <描述当前版本功能>\n */\nclass StrokeAreaFilter(private val ksize:Double = 10.0):BaseFilter() {\n\n    private val d02 = (150 * 150).toDouble()\n\n    override fun doFilter(srcImage: BufferedImage, dstImage: BufferedImage): BufferedImage {\n\n        val outPixels = IntArray(width * height)\n\n        var index = 0\n        var index2 = 0\n        val semiRow = (ksize / 2).toInt()\n        val semiCol = (ksize / 2).toInt()\n        var newX: Int\n        var newY: Int\n\n        // initialize the color RGB array with zero...\n        val rgb = IntArray(3)\n        val rgb2 = IntArray(3)\n        for (i in rgb.indices) {\n            rgb2[i] = 0\n            rgb[i] = rgb2[i]\n        }\n\n        // start the algorithm process here\n        for (row in 0 until height) {\n            var ta = 0\n            for (col in 0 until width) {\n                index = row * width + col\n                ta = inPixels[index] shr 24 and 0xff\n                rgb[0] = inPixels[index] shr 16 and 0xff\n                rgb[1] = inPixels[index] shr 8 and 0xff\n                rgb[2] = inPixels[index] and 0xff\n\n                /* adjust region to fit in source image */\n                // color difference and moment Image\n                var moment = 0.0\n                for (subRow in -semiRow..semiRow) {\n                    for (subCol in -semiCol..semiCol) {\n                        newY = row + subRow\n                        newX = col + subCol\n                        if (newY < 0) {\n                            newY = 0\n                        }\n                        if (newX < 0) {\n                            newX = 0\n                        }\n                        if (newY >= height) {\n                            newY = height - 1\n                        }\n                        if (newX >= width) {\n                            newX = width - 1\n                        }\n                        index2 = newY * width + newX\n                        rgb2[0] = inPixels[index2] shr 16 and 0xff // red\n                        rgb2[1] = inPixels[index2] shr 8 and 0xff // green\n                        rgb2[2] = inPixels[index2] and 0xff // blue\n                        moment += colorDiff(rgb, rgb2)\n                    }\n                }\n                // calculate the output pixel value.\n                val outPixelValue: Int = clamp((255.0 * moment / (ksize * ksize)).toInt())\n                outPixels[index] = ta shl 24 or (outPixelValue shl 16) or (outPixelValue shl 8) or outPixelValue\n            }\n        }\n\n        setRGB(dstImage, 0, 0, width, height, outPixels)\n        return dstImage\n    }\n\n    private fun colorDiff(rgb1: IntArray, rgb2: IntArray): Double {\n        // (1-(d/d0)^2)^2\n        val d2: Double\n        val r2: Double\n        d2 = colorDistance(rgb1, rgb2)\n        if (d2 >= d02) return 0.0\n        r2 = d2 / d02\n        return (1.0 - r2) * (1.0 - r2)\n    }\n\n    private fun colorDistance(rgb1: IntArray, rgb2: IntArray): Double {\n        val dr: Int\n        val dg: Int\n        val db: Int\n        dr = rgb1[0] - rgb2[0]\n        dg = rgb1[1] - rgb2[1]\n        db = rgb1[2] - rgb2[2]\n        return (dr * dr + dg * dg + db * db).toDouble()\n    }\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/filter/SwimFilter.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.filter\n\nimport cn.netdiscovery.monica.imageprocess.filter.base.TransformFilter\nimport cn.netdiscovery.monica.imageprocess.math.Noise\nimport kotlin.math.cos\nimport kotlin.math.sin\n\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.filter.SwimFilter\n * @author: Tony Shen\n * @date: 2025/3/19 19:52\n * @version: V1.0 <描述当前版本功能>\n */\nclass SwimFilter(private val scale:Float = 32f, private val stretch:Float = 1.0f,\n                 private val angle:Float = 0f, private val amount:Float = 1.0f,\n                 private val turbulence:Float = 1.0f, private val time:Float = 0.0f): TransformFilter() {\n\n    private var m00 = 1.0f\n    private var m01 = 0.0f\n    private var m10 = 0.0f\n    private var m11 = 1.0f\n\n    init {\n        val cos = cos(angle.toDouble()).toFloat()\n        val sin = sin(angle.toDouble()).toFloat()\n        m00 = cos\n        m01 = sin\n        m10 = -sin\n        m11 = cos\n    }\n\n    override fun transformInverse(x: Int, y: Int, out: FloatArray) {\n        var nx = m00 * x + m01 * y\n        var ny = m10 * x + m11 * y\n        nx /= scale\n        ny /= scale * stretch\n\n        if (turbulence == 1.0f) {\n            out[0] = x + amount * Noise.noise3(nx + 0.5f, ny, time)\n            out[1] = y + amount * Noise.noise3(nx, ny + 0.5f, time)\n        } else {\n            out[0] = x + amount * Noise.turbulence3(nx + 0.5f, ny, turbulence, time)\n            out[1] = y + amount * Noise.turbulence3(nx, ny + 0.5f, turbulence, time)\n        }\n    }\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/filter/VignetteFilter.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.filter\n\nimport cn.netdiscovery.monica.imageprocess.filter.base.BaseFilter\nimport cn.netdiscovery.monica.imageprocess.utils.clamp\nimport java.awt.Color\nimport java.awt.image.BufferedImage\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.filter.VignetteFilter\n * @author: Tony Shen\n * @date: 2024/5/29 12:50\n * @version: V1.0 <描述当前版本功能>\n */\nclass VignetteFilter(\n    private val fade:Int = 35,\n    private val vignetteWidth:Int = 50\n): BaseFilter() {\n\n    private val vignetteColor: Color = Color.black\n\n    override fun doFilter(srcImage: BufferedImage, dstImage: BufferedImage): BufferedImage {\n\n        val outPixels = IntArray(width * height)\n\n        var index = 0\n        for (row in 0 until height) {\n            var ta = 0\n            var tr = 0\n            var tg = 0\n            var tb = 0\n            for (col in 0 until width) {\n                val dX = Math.min(col, width - col)\n                val dY = Math.min(row, height - row)\n                index = row * width + col\n                ta = inPixels[index] shr 24 and 0xff\n                tr = inPixels[index] shr 16 and 0xff\n                tg = inPixels[index] shr 8 and 0xff\n                tb = inPixels[index] and 0xff\n                if ((dY <= vignetteWidth) and (dX <= vignetteWidth)) {\n                    val k = 1 - (dY.coerceAtMost(dX) - vignetteWidth + fade).toDouble() / fade.toDouble()\n                    outPixels[index] = superpositionColor(ta, tr, tg, tb, k)\n                    continue\n                }\n                if ((dX < vignetteWidth - fade) or (dY < vignetteWidth - fade)) {\n                    outPixels[index] = ta shl 24 or (vignetteColor.red.toInt() shl 16) or (vignetteColor.green.toInt() shl 8) or vignetteColor.blue.toInt()\n                } else {\n                    if ((dX < vignetteWidth) and (dY > vignetteWidth)) {\n                        val k = 1 - (dX - vignetteWidth + fade).toDouble() / fade.toDouble()\n                        outPixels[index] = superpositionColor(ta, tr, tg, tb, k)\n                    } else {\n                        if ((dY < vignetteWidth) and (dX > vignetteWidth)) {\n                            val k = 1 - (dY - vignetteWidth + fade).toDouble() / fade.toDouble()\n                            outPixels[index] = superpositionColor(ta, tr, tg, tb, k)\n                        } else {\n                            outPixels[index] = ta shl 24 or (tr shl 16) or (tg shl 8) or tb\n                        }\n                    }\n                }\n            }\n        }\n\n        setRGB(dstImage, 0, 0, width, height, outPixels)\n        return dstImage\n    }\n\n    private fun superpositionColor(\n        ta: Int,\n        tr: Int,\n        tg: Int,\n        tb: Int,\n        k: Double\n    ): Int {\n        var red = tr\n        var green = tg\n        var blue = tb\n        red = (vignetteColor.red * k + red * (1.0 - k)).toInt()\n        green = (vignetteColor.green * k + green * (1.0 - k)).toInt()\n        blue = (vignetteColor.blue * k + blue * (1.0 - k)).toInt()\n        return ta shl 24 or (clamp(red) shl 16) or (clamp(green) shl 8) or clamp(blue)\n    }\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/filter/WaterFilter.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.filter\n\nimport cn.netdiscovery.monica.imageprocess.filter.base.TransformFilter\nimport cn.netdiscovery.monica.imageprocess.math.TWO_PI\nimport java.awt.image.BufferedImage\nimport kotlin.math.min\nimport kotlin.math.sin\nimport kotlin.math.sqrt\n\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.filter.WaterFilter\n * @author: Tony Shen\n * @date: 2025/3/24 15:45\n * @version: V1.0 <描述当前版本功能>\n */\nclass WaterFilter(private val wavelength:Float = 16f,\n                  private val amplitude:Float = 10f,\n                  private val phase:Float = 0f,\n                  private val centreX:Float = 0.5f,\n                  private val centreY:Float = 0.5f,\n                  private var radius:Float = 50f): TransformFilter() {\n\n    private var radius2 = 0f\n    private var icentreX = 0f\n    private var icentreY = 0f\n\n    init {\n        edgeAction = CLAMP\n    }\n\n    override fun doFilter(srcImage: BufferedImage, dstImage: BufferedImage): BufferedImage {\n        icentreX = width * centreX\n        icentreY = height * centreY\n        if (radius == 0f)\n            radius = min(icentreX.toDouble(), icentreY.toDouble()).toFloat()\n        radius2 = radius * radius\n        return super.doFilter(srcImage, dstImage)\n    }\n\n    override fun transformInverse(x: Int, y: Int, out: FloatArray) {\n        val dx = x - icentreX\n        val dy = y - icentreY\n        val distance2 = dx * dx + dy * dy\n        if (distance2 > radius2) {\n            out[0] = x.toFloat()\n            out[1] = y.toFloat()\n        } else {\n            val distance = sqrt(distance2.toDouble()).toFloat()\n            var amount = amplitude * sin(distance / wavelength * TWO_PI - phase)\n            amount *= (radius - distance) / radius\n            if (distance != 0f) amount *= wavelength / distance\n            out[0] = x + dx * amount\n            out[1] = y + dy * amount\n        }\n    }\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/filter/WhiteImageFilter.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.filter\n\nimport cn.netdiscovery.monica.imageprocess.filter.base.BaseFilter\nimport java.awt.Color\nimport java.awt.image.BufferedImage\nimport kotlin.math.ln\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.filter.WhiteImageFilter\n * @author: Tony Shen\n * @date: 2024/5/1 12:27\n * @version: V1.0 <描述当前版本功能>\n */\nclass WhiteImageFilter(private val beta:Double = 1.1): BaseFilter() {\n\n    override fun doFilter(srcImage: BufferedImage, dstImage: BufferedImage): BufferedImage {\n        // make LUT\n        val lut = IntArray(256)\n        for (i in 0..255) {\n            lut[i] = imageMath(i)\n        }\n\n        for (row in 0 until height) {\n            for (col in 0 until width) {\n                val rgb = srcImage.getRGB(col,row)\n\n                var r = rgb and (0x00ff0000 shr 16)\n                var g = rgb and (0x0000ff00 shr 8)\n                var b = rgb and 0x000000ff\n\n                r = lut[r and 0xff]\n                g = lut[g and 0xff]\n                b = lut[b and 0xff]\n                dstImage.setRGB(col, row, Color(r, g, b).rgb)\n            }\n        }\n\n        return dstImage\n    }\n\n    private fun imageMath(gray: Int): Int {\n        val scale = 255 / (ln(255 * (this.beta - 1) + 1) / ln(this.beta))\n        val p1 = ln(gray * (this.beta - 1) + 1)\n        val np = p1 / ln(this.beta)\n        return (np * scale).toInt()\n    }\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/filter/base/BaseFilter.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.filter.base\n\nimport cn.netdiscovery.monica.imageprocess.BufferedImages\nimport cn.netdiscovery.monica.imageprocess.Transformer\nimport java.awt.image.BufferedImage\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.filter.base.BaseFilter\n * @author: Tony Shen\n * @date: 2024/4/27 13:32\n * @version: V1.0 <描述当前版本功能>\n */\nabstract class BaseFilter: Transformer {\n\n    protected var width = 0\n    protected var height = 0\n    protected var type = 0\n    protected var size = 0\n\n    protected lateinit var inPixels: IntArray\n\n    override fun transform(image: BufferedImage): BufferedImage {\n        width  = image.width\n        height = image.height\n        type = image.type\n\n        size = width * height\n        inPixels = IntArray(size)\n        getRGB(image, 0, 0, width, height, inPixels)\n\n        val dstImage = BufferedImages.create(width,height,type)\n\n        return doFilter(image,dstImage)\n    }\n\n    abstract fun doFilter(srcImage: BufferedImage, dstImage: BufferedImage): BufferedImage\n\n    /**\n     * A convenience method for getting ARGB pixels from an image. This tries to avoid the performance\n     * penalty of BufferedImage.getRGB unmanaging the image.\n     */\n    fun getRGB(image: BufferedImage, x: Int, y: Int, width: Int, height: Int, pixels: IntArray?): IntArray {\n        val type = image.type\n        return if (type == BufferedImage.TYPE_INT_ARGB || type == BufferedImage.TYPE_INT_RGB) image.raster.getDataElements(\n            x,\n            y,\n            width,\n            height,\n            pixels\n        ) as IntArray else image.getRGB(x, y, width, height, pixels, 0, width)\n    }\n\n    /**\n     * A convenience method for setting ARGB pixels in an image. This tries to avoid the performance\n     * penalty of BufferedImage.setRGB unmanaging the image.\n     */\n    fun setRGB(image: BufferedImage, x: Int, y: Int, width: Int, height: Int, pixels: IntArray) {\n        val type = image.type\n        if (type == BufferedImage.TYPE_INT_ARGB || type == BufferedImage.TYPE_INT_RGB) image.raster.setDataElements(\n            x,\n            y,\n            width,\n            height,\n            pixels\n        ) else image.setRGB(x, y, width, height, pixels, 0, width)\n    }\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/filter/base/ColorProcessorFilter.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.filter.base\n\nimport cn.netdiscovery.monica.imageprocess.BufferedImages\nimport java.awt.image.BufferedImage\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.filter.base.ColorProcessorFilter\n * @author: Tony Shen\n * @date: 2024/5/8 19:57\n * @version: V1.0 <描述当前版本功能>\n */\nabstract class ColorProcessorFilter:BaseFilter() {\n\n    protected lateinit var R: ByteArray\n    protected lateinit var G: ByteArray\n    protected lateinit var B: ByteArray\n\n    override fun doFilter(srcImage: BufferedImage, dstImage: BufferedImage): BufferedImage {\n\n        R = ByteArray(size)\n        G = ByteArray(size)\n        B = ByteArray(size)\n        getRGB(inPixels,R,G,B)\n\n        return doColorProcessor(dstImage)\n    }\n\n    abstract fun doColorProcessor(dstImage: BufferedImage):BufferedImage\n\n    /** Returns the red, green and blue planes as 3 byte arrays.  */\n    fun getRGB(pixels: IntArray, R: ByteArray, G: ByteArray, B: ByteArray) {\n        var c: Int\n        var r: Int\n        var g: Int\n        var b: Int\n        val length = pixels.size\n        for (i in 0 until length) {\n            c = pixels[i]\n            r = c and 0xff0000 shr 16\n            g = c and 0xff00 shr 8\n            b = c and 0xff\n            R[i] = r.toByte()\n            G[i] = g.toByte()\n            B[i] = b.toByte()\n        }\n    }\n\n    fun setRGB(width: Int, height: Int, pixels: IntArray, R: ByteArray, G: ByteArray, B: ByteArray) {\n        val size = width * height\n        for (i in 0 until size)\n            pixels[i] = -0x1000000 or (R[i].toInt() and 0xff shl 16) or (G[i].toInt() and 0xff shl 8) or (B[i].toInt() and 0xff)\n    }\n\n    fun setRGB(image: BufferedImage, width: Int, height: Int, pixels: IntArray, R: ByteArray, G: ByteArray, B: ByteArray) {\n        val size = width * height\n        for (i in 0 until size)\n            pixels[i] = -0x1000000 or (R[i].toInt() and 0xff shl 16) or (G[i].toInt() and 0xff shl 8) or (B[i].toInt() and 0xff)\n\n        setRGB(image, 0, 0, width, height, pixels)\n    }\n\n    fun toBufferedImage(bitmap:BufferedImage ?= null): BufferedImage {\n        var pixels:IntArray? = IntArray(width * height)\n        val dst = bitmap ?: BufferedImages.create(width,height,type)\n        setRGB(width, height, pixels!!, R, G, B)\n        setRGB(dst, 0, 0, width, height, pixels)\n        pixels = null\n        return dst\n    }\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/filter/base/ConvolveFilter.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.filter.base\n\n\nimport cn.netdiscovery.monica.imageprocess.utils.clamp\nimport cn.netdiscovery.monica.imageprocess.utils.premultiply\nimport cn.netdiscovery.monica.imageprocess.utils.unpremultiply\nimport java.awt.image.BufferedImage\nimport java.awt.image.Kernel\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.filter.base.ConvolveFilter\n * @author: Tony Shen\n * @date: 2024/5/5 17:54\n * @version: V1.0 <描述当前版本功能>\n */\nopen class ConvolveFilter(private val kernel: Kernel): BaseFilter() {\n\n    /**\n     * Treat pixels off the edge as zero.\n     */\n    var ZERO_EDGES = 0\n\n    /**\n     * Clamp pixels off the edge to the nearest edge.\n     */\n    var CLAMP_EDGES = 1\n\n    /**\n     * Wrap pixels off the edge to the opposite edge.\n     */\n    var WRAP_EDGES = 2\n\n    /**\n     * Whether to convolve alpha.\n     */\n    protected var alpha = true\n\n    /**\n     * Whether to promultiply the alpha before convolving.\n     */\n    protected var premultiplyAlpha = true\n\n    constructor():this(FloatArray(9)) {\n    }\n\n    constructor(matrix: FloatArray): this(Kernel(3, 3, matrix)) {\n    }\n\n    constructor(rows: Int, cols: Int, matrix: FloatArray) : this(Kernel(cols, rows, matrix))\n\n    /**\n     * What do do at the image edges.\n     */\n    private val edgeAction = CLAMP_EDGES\n\n    override fun doFilter(srcImage: BufferedImage, dstImage: BufferedImage): BufferedImage {\n        val inPixels = IntArray(width * height)\n        val outPixels = IntArray(width * height)\n        getRGB(srcImage, 0, 0, width, height, inPixels)\n\n        if (premultiplyAlpha)\n            premultiply(inPixels, 0, inPixels.size)\n\n        convolve(kernel!!, inPixels, outPixels, width, height, alpha, edgeAction)\n\n        if (premultiplyAlpha)\n            unpremultiply(outPixels, 0, outPixels.size)\n\n        setRGB(dstImage, 0, 0, width, height, outPixels)\n        return dstImage\n    }\n\n    /**\n     * Convolve a block of pixels.\n     * @param kernel the kernel\n     * @param inPixels the input pixels\n     * @param outPixels the output pixels\n     * @param width the width\n     * @param height the height\n     * @param edgeAction what to do at the edges\n     */\n    open fun convolve(\n        kernel: Kernel,\n        inPixels: IntArray,\n        outPixels: IntArray,\n        width: Int,\n        height: Int,\n        edgeAction: Int\n    ) {\n        convolve(kernel, inPixels, outPixels, width, height, true, edgeAction)\n    }\n\n    /**\n     * Convolve a block of pixels.\n     * @param kernel the kernel\n     * @param inPixels the input pixels\n     * @param outPixels the output pixels\n     * @param width the width\n     * @param height the height\n     * @param alpha include alpha channel\n     * @param edgeAction what to do at the edges\n     */\n    open fun convolve(\n        kernel: Kernel,\n        inPixels: IntArray,\n        outPixels: IntArray,\n        width: Int,\n        height: Int,\n        alpha: Boolean,\n        edgeAction: Int\n    ) {\n        if (kernel.height == 1)\n            convolveH(kernel, inPixels, outPixels, width, height, alpha, edgeAction)\n        else if (kernel.width == 1)\n            convolveV(kernel, inPixels, outPixels, width, height, alpha, edgeAction)\n        else\n            convolveHV(kernel, inPixels, outPixels, width, height, alpha, edgeAction)\n    }\n\n    /**\n     * Convolve with a 2D kernel.\n     * @param kernel the kernel\n     * @param inPixels the input pixels\n     * @param outPixels the output pixels\n     * @param width the width\n     * @param height the height\n     * @param alpha include alpha channel\n     * @param edgeAction what to do at the edges\n     */\n    open fun convolveHV(\n        kernel: Kernel,\n        inPixels: IntArray,\n        outPixels: IntArray,\n        width: Int,\n        height: Int,\n        alpha: Boolean,\n        edgeAction: Int\n    ) {\n        var index = 0\n        val matrix = kernel.getKernelData(null)\n        val rows = kernel.height\n        val cols = kernel.width\n        val rows2 = rows / 2\n        val cols2 = cols / 2\n        for (y in 0 until height) {\n            for (x in 0 until width) {\n                var r = 0f\n                var g = 0f\n                var b = 0f\n                var a = 0f\n                for (row in -rows2..rows2) {\n                    val iy = y + row\n                    var ioffset: Int\n                    ioffset =\n                        if (0 <= iy && iy < height) iy * width else if (edgeAction == CLAMP_EDGES) y * width else if (edgeAction == WRAP_EDGES) (iy + height) % height * width else continue\n                    val moffset = cols * (row + rows2) + cols2\n                    for (col in -cols2..cols2) {\n                        val f = matrix[moffset + col]\n                        if (f != 0f) {\n                            var ix = x + col\n                            if (!(0 <= ix && ix < width)) {\n                                ix =\n                                    if (edgeAction == CLAMP_EDGES) x else if (edgeAction == WRAP_EDGES) (x + width) % width else continue\n                            }\n                            val rgb = inPixels[ioffset + ix]\n                            a += f * (rgb shr 24 and 0xff)\n                            r += f * (rgb shr 16 and 0xff)\n                            g += f * (rgb shr 8 and 0xff)\n                            b += f * (rgb and 0xff)\n                        }\n                    }\n                }\n                val ia = if (alpha) clamp((a + 0.5).toInt()) else 0xff\n                val ir: Int = clamp((r + 0.5).toInt())\n                val ig: Int = clamp((g + 0.5).toInt())\n                val ib: Int = clamp((b + 0.5).toInt())\n                outPixels[index++] = ia shl 24 or (ir shl 16) or (ig shl 8) or ib\n            }\n        }\n    }\n\n    /**\n     * Convolve with a kernel consisting of one row.\n     * @param kernel the kernel\n     * @param inPixels the input pixels\n     * @param outPixels the output pixels\n     * @param width the width\n     * @param height the height\n     * @param alpha include alpha channel\n     * @param edgeAction what to do at the edges\n     */\n    open fun convolveH(\n        kernel: Kernel,\n        inPixels: IntArray,\n        outPixels: IntArray,\n        width: Int,\n        height: Int,\n        alpha: Boolean,\n        edgeAction: Int\n    ) {\n        var index = 0\n        val matrix = kernel.getKernelData(null)\n        val cols = kernel.width\n        val cols2 = cols / 2\n        for (y in 0 until height) {\n            val ioffset = y * width\n            for (x in 0 until width) {\n                var r = 0f\n                var g = 0f\n                var b = 0f\n                var a = 0f\n                for (col in -cols2..cols2) {\n                    val f = matrix[cols2 + col]\n                    if (f != 0f) {\n                        var ix = x + col\n                        if (ix < 0) {\n                            if (edgeAction == CLAMP_EDGES) ix = 0 else if (edgeAction == WRAP_EDGES) ix =\n                                (x + width) % width\n                        } else if (ix >= width) {\n                            if (edgeAction == CLAMP_EDGES) ix = width - 1 else if (edgeAction == WRAP_EDGES) ix =\n                                (x + width) % width\n                        }\n                        val rgb = inPixels[ioffset + ix]\n                        a += f * (rgb shr 24 and 0xff)\n                        r += f * (rgb shr 16 and 0xff)\n                        g += f * (rgb shr 8 and 0xff)\n                        b += f * (rgb and 0xff)\n                    }\n                }\n                val ia = if (alpha) clamp((a + 0.5).toInt()) else 0xff\n                val ir: Int = clamp((r + 0.5).toInt())\n                val ig: Int = clamp((g + 0.5).toInt())\n                val ib: Int = clamp((b + 0.5).toInt())\n                outPixels[index++] = ia shl 24 or (ir shl 16) or (ig shl 8) or ib\n            }\n        }\n    }\n\n    /**\n     * Convolve with a kernel consisting of one column.\n     * @param kernel the kernel\n     * @param inPixels the input pixels\n     * @param outPixels the output pixels\n     * @param width the width\n     * @param height the height\n     * @param alpha include alpha channel\n     * @param edgeAction what to do at the edges\n     */\n    open fun convolveV(\n        kernel: Kernel,\n        inPixels: IntArray,\n        outPixels: IntArray,\n        width: Int,\n        height: Int,\n        alpha: Boolean,\n        edgeAction: Int\n    ) {\n        var index = 0\n        val matrix = kernel.getKernelData(null)\n        val rows = kernel.height\n        val rows2 = rows / 2\n        for (y in 0 until height) {\n            for (x in 0 until width) {\n                var r = 0f\n                var g = 0f\n                var b = 0f\n                var a = 0f\n                for (row in -rows2..rows2) {\n                    val iy = y + row\n                    var ioffset: Int\n                    ioffset = if (iy < 0) {\n                        if (edgeAction == CLAMP_EDGES) 0 else if (edgeAction == WRAP_EDGES) (y + height) % height * width else iy * width\n                    } else if (iy >= height) {\n                        if (edgeAction == CLAMP_EDGES) (height - 1) * width else if (edgeAction == WRAP_EDGES) (y + height) % height * width else iy * width\n                    } else iy * width\n                    val f = matrix[row + rows2]\n                    if (f != 0f) {\n                        val rgb = inPixels[ioffset + x]\n                        a += f * (rgb shr 24 and 0xff)\n                        r += f * (rgb shr 16 and 0xff)\n                        g += f * (rgb shr 8 and 0xff)\n                        b += f * (rgb and 0xff)\n                    }\n                }\n                val ia = if (alpha) clamp((a + 0.5).toInt()) else 0xff\n                val ir: Int = clamp((r + 0.5).toInt())\n                val ig: Int = clamp((g + 0.5).toInt())\n                val ib: Int = clamp((b + 0.5).toInt())\n                outPixels[index++] = ia shl 24 or (ir shl 16) or (ig shl 8) or ib\n            }\n        }\n    }\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/filter/base/PointFilter.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.filter.base\n\nimport java.awt.image.BufferedImage\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.filter.base.PointFilter\n * @author: Tony Shen\n * @date: 2025/3/11 21:00\n * @version: V1.0 <描述当前版本功能>\n */\nabstract class PointFilter: BaseFilter() {\n\n    protected var canFilterIndexColorModel: Boolean = false\n\n    override fun doFilter(srcImage: BufferedImage, dstImage: BufferedImage): BufferedImage {\n        setDimensions(width, height)\n\n        val outPixels = IntArray(width * height)\n\n        var index = 0\n        for (row in 0 until height) {\n            for (col in 0 until width) {\n                index = row * width + col\n                outPixels[index] = filterRGB(col, row, inPixels[index])\n            }\n        }\n\n        setRGB(dstImage, 0, 0, width, height, outPixels)\n\n        return dstImage\n    }\n\n    open fun setDimensions(width: Int, height: Int) {\n    }\n\n    abstract fun filterRGB(x: Int, y: Int, rgb: Int): Int\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/filter/base/TransferFilter.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.filter.base\n\nimport cn.netdiscovery.monica.imageprocess.utils.clamp\n\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.filter.base.TransferFilter\n * @author: Tony Shen\n * @date: 2025/3/12 17:58\n * @version: V1.0 <描述当前版本功能>\n */\nabstract class TransferFilter : PointFilter(){\n\n    protected lateinit var rTable: IntArray\n    protected lateinit var gTable: IntArray\n    protected lateinit var bTable: IntArray\n\n    init {\n        canFilterIndexColorModel = true\n    }\n\n    private fun makeTable(): IntArray {\n        val table = IntArray(256)\n        for (i in 0..255) {\n            table[i] = clamp((255 * transferFunction(i / 255.0f)).toInt())\n        }\n\n        return table\n    }\n\n    abstract fun transferFunction(v: Float): Float\n\n    override fun filterRGB(x: Int, y: Int, rgb: Int): Int {\n        rTable = makeTable()\n        gTable = rTable\n        bTable = rTable\n\n        val a = rgb and 0xff000000.toInt()\n        var r = (rgb shr 16) and 0xff\n        var g = (rgb shr 8) and 0xff\n        var b = rgb and 0xff\n\n        r = rTable[r]\n        g = gTable[g]\n        b = bTable[b]\n        return a or (r shl 16) or (g shl 8) or b\n    }\n\n//    fun getLUT(): IntArray {\n//        val lut = IntArray(256)\n//        for (i in 0..255) {\n//            lut[i] = filterRGB(0, 0, (i shl 24) or (i shl 16) or (i shl 8) or i)\n//        }\n//        return lut\n//    }\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/filter/base/TransformFilter.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.filter.base\n\nimport cn.netdiscovery.monica.imageprocess.math.bilinearInterpolate\nimport cn.netdiscovery.monica.imageprocess.utils.clamp\nimport cn.netdiscovery.monica.imageprocess.math.mod\nimport java.awt.Rectangle\nimport java.awt.image.BufferedImage\nimport kotlin.math.floor\n\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.filter.base.TransformFilter\n * @author: Tony Shen\n * @date:  2025/3/8 13:45\n * @version: V1.0 <描述当前版本功能>\n */\nabstract class TransformFilter: BaseFilter() {\n\n    /**\n     * Treat pixels off the edge as zero.\n     */\n    val ZERO: Int = 0\n\n    /**\n     * Clamp pixels to the image edges.\n     */\n    val CLAMP: Int = 1\n\n    /**\n     * Wrap pixels off the edge onto the oppsoite edge.\n     */\n    val WRAP: Int = 2\n\n    /**\n     * Clamp pixels RGB to the image edges, but zero the alpha. This prevents gray borders on your image.\n     */\n    val RGB_CLAMP: Int = 3\n\n    /**\n     * Use nearest-neighbout interpolation.\n     */\n    val NEAREST_NEIGHBOUR: Int = 0\n\n    /**\n     * Use bilinear interpolation.\n     */\n    val BILINEAR: Int = 1\n\n    /**\n     * The action to take for pixels off the image edge.\n     */\n    protected var edgeAction: Int = RGB_CLAMP\n\n    /**\n     * The type of interpolation to use.\n     */\n    protected var interpolation: Int = BILINEAR\n\n    /**\n     * The output image rectangle.\n     */\n    protected var transformedSpace: Rectangle? = null\n\n    /**\n     * The input image rectangle.\n     */\n    protected var originalSpace: Rectangle? = null\n\n    /**\n     * Inverse transform a point. This method needs to be overriden by all subclasses.\n     * @param x the X position of the pixel in the output image\n     * @param y the Y position of the pixel in the output image\n     * @param out the position of the pixel in the input image\n     */\n    abstract fun transformInverse(x: Int, y: Int, out: FloatArray)\n\n    /**\n     * Forward transform a rectangle. Used to determine the size of the output image.\n     * @param rect the rectangle to transform\n     */\n    protected open fun transformSpace(rect: Rectangle) {\n    }\n\n    override fun doFilter(srcImage: BufferedImage, dstImage: BufferedImage): BufferedImage {\n        var dst = dstImage\n        val width = srcImage.width\n        val height = srcImage.height\n        val type = srcImage.type\n        val srcRaster = srcImage.raster\n\n        originalSpace = Rectangle(0, 0, width, height)\n        transformedSpace = Rectangle(0, 0, width, height)\n        transformSpace(transformedSpace!!)\n\n//        val dstRaster = dst.raster\n\n        val inPixels: IntArray = getRGB(srcImage, 0, 0, width, height, null)\n\n        if (interpolation == NEAREST_NEIGHBOUR) return filterPixelsNN(dst, width, height, inPixels, transformedSpace!!)\n\n        val srcWidth = width\n        val srcHeight = height\n        val srcWidth1 = width - 1\n        val srcHeight1 = height - 1\n        val outWidth = transformedSpace!!.width\n        val outHeight = transformedSpace!!.height\n        val index = 0\n        val outPixels = IntArray(outWidth)\n\n        val outX = transformedSpace!!.x\n        val outY = transformedSpace!!.y\n        val out = FloatArray(2)\n\n        for (y in 0 until outHeight) {\n            for (x in 0 until outWidth) {\n                transformInverse(outX + x, outY + y, out)\n                val srcX = floor(out[0].toDouble()).toInt()\n                val srcY = floor(out[1].toDouble()).toInt()\n                val xWeight = out[0] - srcX\n                val yWeight = out[1] - srcY\n                var nw: Int\n                var ne: Int\n                var sw: Int\n                var se: Int\n\n                if (srcX >= 0 && srcX < srcWidth1 && srcY >= 0 && srcY < srcHeight1) {\n                    // Easy case, all corners are in the image\n                    val i = srcWidth * srcY + srcX\n                    nw = inPixels[i]\n                    ne = inPixels[i + 1]\n                    sw = inPixels[i + srcWidth]\n                    se = inPixels[i + srcWidth + 1]\n                } else {\n                    // Some of the corners are off the image\n                    nw = getPixel(inPixels, srcX, srcY, srcWidth, srcHeight)\n                    ne = getPixel(inPixels, srcX + 1, srcY, srcWidth, srcHeight)\n                    sw = getPixel(inPixels, srcX, srcY + 1, srcWidth, srcHeight)\n                    se = getPixel(inPixels, srcX + 1, srcY + 1, srcWidth, srcHeight)\n                }\n                outPixels[x] = bilinearInterpolate(xWeight, yWeight, nw, ne, sw, se)\n            }\n            setRGB(dst, 0, y, transformedSpace!!.width, 1, outPixels)\n        }\n        return dst\n    }\n\n    private fun getPixel(pixels: IntArray, x: Int, y: Int, width: Int, height: Int): Int {\n        if (x < 0 || x >= width || y < 0 || y >= height) {\n            return when (edgeAction) {\n                ZERO -> 0\n                WRAP -> pixels[mod(y, height) * width + mod(x, width)]\n                CLAMP -> pixels[clamp(y, 0, height - 1) * width + clamp(x, 0, width - 1)]\n                RGB_CLAMP -> pixels[clamp(y, 0, height - 1) * width + clamp(x, 0, width - 1)] and 0x00ffffff\n\n                else -> 0\n            }\n        }\n        return pixels[y * width + x]\n    }\n\n    protected fun filterPixelsNN(\n        dst: BufferedImage,\n        width: Int,\n        height: Int,\n        inPixels: IntArray,\n        transformedSpace: Rectangle\n    ): BufferedImage {\n        val srcWidth = width\n        val srcHeight = height\n        val outWidth = transformedSpace.width\n        val outHeight = transformedSpace.height\n        var srcX: Int\n        var srcY: Int\n        val outPixels = IntArray(outWidth)\n\n        val outX = transformedSpace.x\n        val outY = transformedSpace.y\n        val rgb = IntArray(4)\n        val out = FloatArray(2)\n\n        for (y in 0 until outHeight) {\n            for (x in 0 until outWidth) {\n                transformInverse(outX + x, outY + y, out)\n                srcX = out[0].toInt()\n                srcY = out[1].toInt()\n                // int casting rounds towards zero, so we check out[0] < 0, not srcX < 0\n                if (out[0] < 0 || srcX >= srcWidth || out[1] < 0 || srcY >= srcHeight) {\n                    var p = when (edgeAction) {\n                        ZERO -> 0\n                        WRAP -> inPixels[mod(srcY, srcHeight) * srcWidth + mod(srcX, srcWidth)]\n                        CLAMP -> inPixels[clamp(srcY, 0, srcHeight - 1) * srcWidth + clamp(srcX, 0, srcWidth - 1)]\n                        RGB_CLAMP -> inPixels[clamp(srcY, 0, srcHeight - 1) * srcWidth + clamp(srcX, 0, srcWidth - 1)] and 0x00ffffff\n\n                        else -> 0\n                    }\n                    outPixels[x] = p\n                } else {\n                    val i = srcWidth * srcY + srcX\n                    rgb[0] = inPixels[i]\n                    outPixels[x] = inPixels[i]\n                }\n            }\n            setRGB(dst, 0, y, transformedSpace.width, 1, outPixels)\n        }\n        return dst\n    }\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/filter/base/WholeImageFilter.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.filter.base\n\nimport java.awt.Rectangle\nimport java.awt.image.BufferedImage\nimport java.awt.image.WritableRaster\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.filter.base.WholeImageFilter\n * @author: Tony Shen\n * @date: 2025/3/20 10:51\n * @version: V1.0 <描述当前版本功能>\n */\nabstract class WholeImageFilter:BaseFilter() {\n\n    /**\n     * The output image bounds.\n     */\n    protected lateinit var transformedSpace: Rectangle\n\n    /**\n     * The input image bounds.\n     */\n    protected lateinit var originalSpace: Rectangle\n\n\n    override fun doFilter(srcImage: BufferedImage, dstImage: BufferedImage): BufferedImage {\n        val srcRaster: WritableRaster = srcImage.raster\n\n        originalSpace = Rectangle(0, 0, width, height)\n        transformedSpace = Rectangle(0, 0, width, height)\n        transformSpace(transformedSpace)\n\n        val dstRaster: WritableRaster = dstImage.raster\n\n        var inPixels = getRGB(srcImage, 0, 0, width, height, null)\n        inPixels = filterPixels(width, height, inPixels, transformedSpace)\n        setRGB(dstImage, 0, 0, transformedSpace.width, transformedSpace.height, inPixels)\n\n        return dstImage\n    }\n\n    /**\n     * Calculate output bounds for given input bounds.\n     * @param rect input and output rectangle\n     */\n    open fun transformSpace(rect: Rectangle) {\n    }\n\n    /**\n     * Actually filter the pixels.\n     * @param width the image width\n     * @param height the image height\n     * @param inPixels the image pixels\n     * @param transformedSpace the output bounds\n     * @return the output pixels\n     */\n    protected abstract fun filterPixels(\n        width: Int,\n        height: Int,\n        inPixels: IntArray,\n        transformedSpace: Rectangle\n    ): IntArray\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/filter/blur/AverageFilter.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.filter.blur\n\nimport cn.netdiscovery.monica.imageprocess.filter.base.ConvolveFilter\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.filter.blur.AverageFilter\n * @author: Tony Shen\n * @date: 2024/5/5 19:17\n * @version: V1.0 <描述当前版本功能>\n */\nclass AverageFilter: ConvolveFilter(matrix) {\n\n    companion object {\n        private val matrix = floatArrayOf(\n            0.1f, 0.1f, 0.1f,\n            0.1f, 0.2f, 0.1f,\n            0.1f, 0.1f, 0.1f\n        )\n    }\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/filter/blur/BoxBlurFilter.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.filter.blur\n\nimport cn.netdiscovery.monica.imageprocess.filter.base.BaseFilter\nimport cn.netdiscovery.monica.imageprocess.utils.clamp\nimport java.awt.image.BufferedImage\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.filter.blur.BoxBlurFilter\n * @author: Tony Shen\n * @date: 2024/4/27 13:36\n * @version: V1.0 <描述当前版本功能>\n */\nclass BoxBlurFilter(private val hRadius: Int =5, private val vRadius:Int=5, private val iterations:Int=1): BaseFilter() {\n\n    override fun doFilter(srcImage: BufferedImage, dstImage: BufferedImage): BufferedImage {\n\n        var outPixels = IntArray(size)\n\n        for (i in 0 until iterations) {\n            blur( inPixels, outPixels, width, height, hRadius )\n            blur( outPixels, inPixels, height, width, vRadius )\n        }\n\n        setRGB(dstImage, 0, 0, width, height, inPixels)\n        return dstImage\n    }\n\n    private fun blur(`in`: IntArray, out: IntArray, width: Int, height: Int, radius: Int) {\n        val widthMinus1 = width - 1\n        val tableSize = 2 * radius + 1\n        val divide = IntArray(256 * tableSize)\n\n        // the value scope will be 0 to 255, and number of 0 is table size\n        // will get means from index not calculate result again since\n        // color value must be  between 0 and 255.\n        for (i in 0 until 256 * tableSize)\n            divide[i] = i / tableSize\n\n        var inIndex = 0\n\n        //\n        for (y in 0 until height) {\n            var outIndex = y\n            var ta = 0\n            var tr = 0\n            var tg = 0\n            var tb = 0 // ARGB -> prepare for the alpha, red, green, blue color value.\n            for (i in -radius..radius) {\n                val rgb = `in`[inIndex + clamp(i, 0, width - 1)] // read input pixel data here. table size data.\n                ta += rgb shr 24 and 0xff\n                tr += rgb shr 16 and 0xff\n                tg += rgb shr 8 and 0xff\n                tb += rgb and 0xff\n            }\n\n            for (x in 0 until width) { // get output pixel data.\n                out[outIndex] = divide[ta] shl 24 or (divide[tr] shl 16) or (divide[tg] shl 8) or divide[tb] // calculate the output data.\n                var i1 = x + radius + 1\n                if (i1 > widthMinus1) i1 = widthMinus1\n                var i2 = x - radius\n                if (i2 < 0) i2 = 0\n                val rgb1 = `in`[inIndex + i1]\n                val rgb2 = `in`[inIndex + i2]\n                ta += (rgb1 shr 24 and 0xff) - (rgb2 shr 24 and 0xff)\n                tr += (rgb1 and 0xff0000) - (rgb2 and 0xff0000) shr 16\n                tg += (rgb1 and 0xff00) - (rgb2 and 0xff00) shr 8\n                tb += (rgb1 and 0xff) - (rgb2 and 0xff)\n                outIndex += height // per column or per row as cycle...\n            }\n            inIndex += width // next (i+ column number * n, n=1....n-1)\n        }\n    }\n\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/filter/blur/FastBlur2D.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.filter.blur\n\nimport cn.netdiscovery.monica.imageprocess.IntIntegralImage\nimport cn.netdiscovery.monica.imageprocess.filter.base.ColorProcessorFilter\nimport cn.netdiscovery.monica.imageprocess.utils.clamp\nimport com.safframework.kotlin.coroutines.asyncInBackground\nimport kotlinx.coroutines.awaitAll\nimport kotlinx.coroutines.runBlocking\nimport java.awt.image.BufferedImage\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.filter.blur.FastBlur2D\n * @author: Tony Shen\n * @date: 2024/6/22 22:35\n * @version: V1.0 <描述当前版本功能>\n */\n\nclass FastBlur2D(private val ksize:Int = 5) : ColorProcessorFilter() {\n\n    override fun doColorProcessor(dstImage: BufferedImage): BufferedImage {\n\n        val radius = ksize / 2\n\n        runBlocking {\n            listOf(\n                asyncInBackground {\n                    var output:ByteArray? = ByteArray(size)\n                    val ii = IntIntegralImage()\n                    System.arraycopy(R, 0, output, 0, size)\n                    ii.setImage(R)\n                    ii.calculate(width, height)\n                    processSingleChannel(width, height,radius, ii, output!!)\n                    System.arraycopy(output, 0, R, 0, size)\n                    output = null\n                },\n                asyncInBackground {\n                    var output:ByteArray? = ByteArray(size)\n                    val ii = IntIntegralImage()\n                    System.arraycopy(G, 0, output, 0, size)\n                    ii.setImage(G)\n                    ii.calculate(width, height)\n                    processSingleChannel(width, height,radius, ii, output!!)\n                    System.arraycopy(output, 0, G, 0, size)\n                    output = null\n                },\n                asyncInBackground {\n                    var output:ByteArray? = ByteArray(size)\n                    val ii = IntIntegralImage()\n                    System.arraycopy(B, 0, output, 0, size)\n                    ii.setImage(B)\n                    ii.calculate(width, height)\n                    processSingleChannel(width, height,radius, ii, output!!)\n                    System.arraycopy(output, 0, B, 0, size)\n                    output = null\n                }\n            ).awaitAll()\n        }\n\n        return toBufferedImage(dstImage)\n    }\n\n    private fun processSingleChannel(w: Int, h: Int, radius:Int, ii: IntIntegralImage, output: ByteArray) {\n        var x2 = 0\n        var y2 = 0\n        var x1 = 0\n        var y1 = 0\n        var cx = 0\n        var cy = 0\n        for (row in 0 until h + radius) {\n            y2 = if (row + 1 > h) h else row + 1\n            y1 = if (row - ksize < 0) 0 else row - ksize\n            for (col in 0 until w + radius) {\n                x2 = if (col + 1 > w) w else col + 1\n                x1 = if (col - ksize < 0) 0 else col - ksize\n                cx = if (col - radius < 0) 0 else col - radius\n                cy = if (row - radius < 0) 0 else row - radius\n                val num = (x2 - x1) * (y2 - y1)\n                val s = ii.getBlockSum(x1, y1, x2, y2)\n                output[cy * w + cx] = clamp(s / num).toByte()\n            }\n        }\n    }\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/filter/blur/GaussianFilter.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.filter.blur\n\nimport cn.netdiscovery.monica.imageprocess.filter.base.BaseFilter\nimport cn.netdiscovery.monica.imageprocess.utils.clamp\nimport java.awt.image.BufferedImage\nimport java.awt.image.Kernel\nimport kotlin.math.PI\nimport kotlin.math.ceil\nimport kotlin.math.exp\n\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.filter.blur.GaussianFilter\n * @author: Tony Shen\n * @date: 2024/4/29 17:40\n * @version: V1.0 <描述当前版本功能>\n */\nopen class GaussianFilter(open val radius:Float = 5.0f): BaseFilter() {\n\n    /**\n     * Treat pixels off the edge as zero.\n     */\n    var ZERO_EDGES = 0\n\n    /**\n     * Clamp pixels off the edge to the nearest edge.\n     */\n    var CLAMP_EDGES = 1\n\n    /**\n     * Wrap pixels off the edge to the opposite edge.\n     */\n    var WRAP_EDGES = 2\n\n    /**\n     * Whether to convolve alpha.\n     */\n    protected var alpha = true\n\n    /**\n     * Whether to promultiply the alpha before convolving.\n     */\n    protected var premultiplyAlpha = true\n\n    /**\n     * The convolution kernel.\n     */\n    protected var kernel: Kernel\n\n    init {\n        kernel = makeKernel(radius)\n    }\n\n    override fun doFilter(srcImage: BufferedImage, dstImage: BufferedImage): BufferedImage {\n        val outPixels = IntArray(width * height)\n\n        if ( radius > 0 ) {\n            convolveAndTranspose(kernel, inPixels, outPixels, width, height, alpha, alpha && premultiplyAlpha, false, CLAMP_EDGES)\n            convolveAndTranspose(kernel, outPixels, inPixels, height, width, alpha, false, alpha && premultiplyAlpha, CLAMP_EDGES)\n        }\n\n        setRGB(dstImage, 0, 0, width, height, inPixels)\n        return dstImage\n    }\n\n    /**\n     * Blur and transpose a block of ARGB pixels.\n     * @param kernel the blur kernel\n     * @param inPixels the input pixels\n     * @param outPixels the output pixels\n     * @param width the width of the pixel array\n     * @param height the height of the pixel array\n     * @param alpha whether to blur the alpha channel\n     * @param edgeAction what to do at the edges\n     */\n    fun convolveAndTranspose(\n        kernel: Kernel,\n        inPixels: IntArray,\n        outPixels: IntArray,\n        width: Int,\n        height: Int,\n        alpha: Boolean,\n        premultiply: Boolean,\n        unpremultiply: Boolean,\n        edgeAction: Int\n    ) {\n        val matrix = kernel.getKernelData(null)\n        val cols = kernel.width\n        val cols2 = cols / 2\n        for (y in 0 until height) {\n            var index = y\n            val ioffset = y * width\n            for (x in 0 until width) {\n                var r = 0f\n                var g = 0f\n                var b = 0f\n                var a = 0f\n                for (col in -cols2..cols2) {\n                    val f = matrix[cols2 + col]\n                    if (f != 0f) {\n                        var ix = x + col\n                        if (ix < 0) {\n                            if (edgeAction == CLAMP_EDGES) ix = 0 else if (edgeAction == WRAP_EDGES) ix =\n                                (x + width) % width\n                        } else if (ix >= width) {\n                            if (edgeAction == CLAMP_EDGES) ix = width - 1 else if (edgeAction == WRAP_EDGES) ix =\n                                (x + width) % width\n                        }\n                        val rgb = inPixels[ioffset + ix]\n                        val pa = rgb shr 24 and 0xff\n                        var pr = rgb shr 16 and 0xff\n                        var pg = rgb shr 8 and 0xff\n                        var pb = rgb and 0xff\n                        if (premultiply) {\n                            val a255 = pa * (1.0f / 255.0f)\n                            pr = (pr * a255).toInt()\n                            pg = (pg * a255).toInt()\n                            pb = (pb * a255).toInt()\n                        }\n                        a += f * pa\n                        r += f * pr\n                        g += f * pg\n                        b += f * pb\n                    }\n                }\n                if (unpremultiply && a != 0f && a != 255f) {\n                    val f = 255.0f / a\n                    r *= f\n                    g *= f\n                    b *= f\n                }\n                val ia = if (alpha) clamp((a + 0.5).toInt()) else 0xff\n                val ir: Int = clamp((r + 0.5).toInt())\n                val ig: Int = clamp((g + 0.5).toInt())\n                val ib: Int = clamp((b + 0.5).toInt())\n                outPixels[index] = ia shl 24 or (ir shl 16) or (ig shl 8) or ib\n                index += height\n            }\n        }\n    }\n\n    private fun makeKernel(radius: Float): Kernel {\n        val r = ceil(radius.toDouble()).toInt()\n        val rows = r * 2 + 1\n        val matrix = FloatArray(rows)\n        val sigma = radius / 3\n        val sigma22 = 2 * sigma * sigma\n        val sigmaPi2: Float = 2 * PI.toFloat() * sigma\n        val sqrtSigmaPi2 = Math.sqrt(sigmaPi2.toDouble()).toFloat()\n        val radius2 = radius * radius\n        var total = 0f\n        for ((index, row) in (-r..r).withIndex()) {\n            val distance = (row * row).toFloat()\n            if (distance > radius2) matrix[index] = 0f else matrix[index] =\n                exp((-distance / sigma22).toDouble()).toFloat() / sqrtSigmaPi2\n            total += matrix[index]\n        }\n        for (i in 0 until rows) matrix[i] /= total\n        return Kernel(rows, 1, matrix)\n    }\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/filter/blur/LensBlurFilter.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.filter.blur\n\nimport cn.netdiscovery.monica.imageprocess.filter.base.BaseFilter\nimport cn.netdiscovery.monica.imageprocess.math.FFT\nimport cn.netdiscovery.monica.imageprocess.math.mod\nimport java.awt.image.BufferedImage\nimport kotlin.math.*\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.filter.blur.LensBlurFilter\n * @author: Tony Shen\n * @date: 2025/3/14 15:39\n * @version: V1.0 <描述当前版本功能>\n */\nclass LensBlurFilter(private val radius:Float = 10f,\n                     private val bloom:Float = 2f,\n                     private val bloomThreshold:Float = 255f,\n                     private val angle:Float = 0f,\n                     private val sides:Int = 5): BaseFilter() {\n\n    override fun doFilter(srcImage: BufferedImage, dstImage: BufferedImage): BufferedImage {\n\n        var rows = 1\n        var cols = 1\n        var log2rows = 0\n        var log2cols = 0\n        val iradius = ceil(radius.toDouble()).toInt()\n        var tileWidth = 128\n        var tileHeight = tileWidth\n\n//        val adjustedWidth = width + iradius * 2\n//        val adjustedHeight = height + iradius * 2\n\n        tileWidth = if (iradius < 32) Math.min(128, width + 2 * iradius) else Math.min(256, width + 2 * iradius)\n        tileHeight = if (iradius < 32) Math.min(128, height + 2 * iradius) else Math.min(256, height + 2 * iradius)\n\n        while (rows < tileHeight) {\n            rows *= 2\n            log2rows++\n        }\n        while (cols < tileWidth) {\n            cols *= 2\n            log2cols++\n        }\n        val w = cols\n        val h = rows\n\n        tileWidth = w\n        tileHeight = h // FIXME - tileWidth, w, 和 cols 始终相同\n\n        val fft = FFT(max(log2rows, log2cols))\n\n        val rgb = IntArray(w * h)\n        val mask = Array(2) { FloatArray(w * h) }\n        val gb = Array(2) { FloatArray(w * h) }\n        val ar = Array(2) { FloatArray(w * h) }\n\n        // 创建核函数\n        val polyAngle = Math.PI / sides\n        val polyScale = 1.0 / Math.cos(polyAngle)\n        val r2 = radius * radius\n        val rangle = Math.toRadians(angle.toDouble())\n        var total = 0f\n        var i = 0\n        for (y in 0 until h) {\n            for (x in 0 until w) {\n                val dx:Double = (x - w / 2f).toDouble()\n                val dy:Double = (y - h / 2f).toDouble()\n                var r:Double = dx * dx + dy * dy\n                var f = if (r < r2) 1.0 else 0.0\n                if (f != 0.0) {\n                    r = Math.sqrt(r)\n                    f = if (sides != 0) {\n                        var a = Math.atan2(dy, dx) + rangle\n                        a = mod(a, polyAngle * 2) - polyAngle\n                        Math.cos(a) * polyScale\n                    } else {\n                        1.0\n                    }\n                    f = if (f * r < radius) 1.0 else 0.0\n                }\n                total += f.toFloat()\n                mask[0][i] = f.toFloat()\n                mask[1][i] = 0f\n                i++\n            }\n        }\n\n        // 归一化核函数\n        i = 0\n        for (y in 0 until h) {\n            for (x in 0 until w) {\n                mask[0][i] /= total\n                i++\n            }\n        }\n\n        fft.transform2D(mask[0], mask[1], w, h, true)\n\n        var tileY = -iradius\n        while (tileY < height) {\n            var tileX = -iradius\n            while (tileX < width) {\n                // 裁剪 tile 区域到图像范围内\n                var tx = tileX\n                var ty = tileY\n                var tw = tileWidth\n                var th = tileHeight\n                var fx = 0\n                var fy = 0\n                if (tx < 0) {\n                    tw += tx\n                    fx -= tx\n                    tx = 0\n                }\n                if (ty < 0) {\n                    th += ty\n                    fy -= ty\n                    ty = 0\n                }\n                if (tx + tw > width)\n                    tw = width - tx\n                if (ty + th > height)\n                    th = height - ty\n\n                srcImage.getRGB(tx, ty, tw, th, rgb, fy * w + fx, w)\n\n                // 根据像素创建浮点数组，图像边界之外的像素使用边缘像素值填充\n                i = 0\n                for (y in 0 until h) {\n                    val imageY = y + tileY\n                    val j = when {\n                        imageY < 0 -> fy\n                        imageY > height -> fy + th - 1\n                        else -> y\n                    } * w\n                    for (x in 0 until w) {\n                        val imageX = x + tileX\n                        val k = when {\n                            imageX < 0 -> fx\n                            imageX > width -> fx + tw - 1\n                            else -> x\n                        } + j\n\n                        ar[0][i] = ((rgb[k] shr 24) and 0xff).toFloat()\n                        var rPixel = ((rgb[k] shr 16) and 0xff).toFloat()\n                        var gPixel = ((rgb[k] shr 8) and 0xff).toFloat()\n                        var bPixel = (rgb[k] and 0xff).toFloat()\n\n                        // Bloom 处理\n                        if (rPixel > bloomThreshold)\n                            rPixel *= bloom\n                        if (gPixel > bloomThreshold)\n                            gPixel *= bloom\n                        if (bPixel > bloomThreshold)\n                            bPixel *= bloom\n\n                        ar[1][i] = rPixel\n                        gb[0][i] = gPixel\n                        gb[1][i] = bPixel\n\n                        i++\n                    }\n                }\n\n                // 转换到频域\n                fft.transform2D(ar[0], ar[1], cols, rows, true)\n                fft.transform2D(gb[0], gb[1], cols, rows, true)\n\n                // 将变换后的像素与变换后的核函数相乘\n                i = 0\n                for (y in 0 until h) {\n                    for (x in 0 until w) {\n                        val re = ar[0][i]\n                        val im = ar[1][i]\n                        val rem = mask[0][i]\n                        val imm = mask[1][i]\n                        ar[0][i] = re * rem - im * imm\n                        ar[1][i] = re * imm + im * rem\n\n                        val reGb = gb[0][i]\n                        val imGb = gb[1][i]\n                        gb[0][i] = reGb * rem - imGb * imm\n                        gb[1][i] = reGb * imm + imGb * rem\n                        i++\n                    }\n                }\n\n                // 逆变换回空域\n                fft.transform2D(ar[0], ar[1], cols, rows, false)\n                fft.transform2D(gb[0], gb[1], cols, rows, false)\n\n                // 将频域数据转换回 RGB 像素，并进行象限重新映射\n                val row_flip = w shr 1\n                val col_flip = h shr 1\n                var index = 0\n                for (y in 0 until w) {\n                    val ym = y xor row_flip\n                    val yi = ym * cols\n                    for (x in 0 until w) {\n                        val xm = yi + (x xor col_flip)\n                        var a = ar[0][xm].toInt()\n                        var r = ar[1][xm].toInt()\n                        var g = gb[0][xm].toInt()\n                        var b = gb[1][xm].toInt()\n\n                        // 限制 Bloom 后过高的像素值\n                        if (r > 255) r = 255\n                        if (g > 255) g = 255\n                        if (b > 255) b = 255\n                        val argb = (a shl 24) or (r shl 16) or (g shl 8) or b\n                        rgb[index++] = argb\n                    }\n                }\n\n                // 将处理后的 tile 裁剪写回输出图像\n                tx = tileX + iradius\n                ty = tileY + iradius\n                tw = tileWidth - 2 * iradius\n                th = tileHeight - 2 * iradius\n                if (tx + tw > width)\n                    tw = width - tx\n                if (ty + th > height)\n                    th = height - ty\n                dstImage.setRGB(tx, ty, tw, th, rgb, iradius * w + iradius, w)\n\n                tileX += tileWidth - 2 * iradius\n            }\n            tileY += tileHeight - 2 * iradius\n        }\n\n        return dstImage\n    }\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/filter/blur/MaximumFilter.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.filter.blur\n\nimport cn.netdiscovery.monica.imageprocess.filter.base.ColorProcessorFilter\nimport java.awt.image.BufferedImage\nimport java.util.*\n\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.filter.blur.MaximumFilter\n * @author: Tony Shen\n * @date: 2025/3/24 14:41\n * @version: V1.0 <描述当前版本功能>\n */\nclass MaximumFilter: ColorProcessorFilter() {\n\n    override fun doColorProcessor(dstImage: BufferedImage): BufferedImage {\n        val numOfPixels = width * height\n        val output: Array<ByteArray> = Array(3) { ByteArray(numOfPixels) }\n\n        val size: Int = 1 * 2 + 1\n        val total = size * size\n        var r = 0\n        var g = 0\n        var b = 0\n        for (row in 0..<height) {\n            for (col in 0..<width) {\n\n                // 统计滤波器\n                val subpixels = Array(3) { IntArray(total) }\n                var index = 0\n                for (i in -1..1) {\n                    var roffset: Int = row + i\n                    roffset = if (roffset < 0) 0 else (if (roffset >= height) height - 1 else roffset)\n                    for (j in -1..1) {\n                        var coffset: Int = col + j\n                        coffset = if (coffset < 0) 0 else (if (coffset >= width) width - 1 else coffset)\n                        subpixels[0][index] = R[roffset * width + coffset].toInt() and 0xff\n                        subpixels[1][index] = G[roffset * width + coffset].toInt() and 0xff\n                        subpixels[2][index] = B[roffset * width + coffset].toInt() and 0xff\n                        index++\n                    }\n                }\n\n                Arrays.sort(subpixels[0])\n                Arrays.sort(subpixels[1])\n                Arrays.sort(subpixels[2])\n\n                r = subpixels[0][total - 1]\n                g = subpixels[1][total - 1]\n                b = subpixels[2][total - 1]\n\n                output[0][row * width + col] = r.toByte()\n                output[1][row * width + col] = g.toByte()\n                output[2][row * width + col] = b.toByte()\n            }\n        }\n\n        R = output[0]\n        G = output[1]\n        B = output[2]\n\n        return toBufferedImage(dstImage)\n    }\n\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/filter/blur/MinimumFilter.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.filter.blur\n\nimport cn.netdiscovery.monica.imageprocess.filter.base.ColorProcessorFilter\nimport java.awt.image.BufferedImage\nimport java.util.*\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.filter.blur.MinimumFilter\n * @author: Tony Shen\n * @date: 2025/3/24 15:31\n * @version: V1.0 <描述当前版本功能>\n */\nclass MinimumFilter: ColorProcessorFilter() {\n\n    override fun doColorProcessor(dstImage: BufferedImage): BufferedImage {\n        val numOfPixels = width * height\n        val output: Array<ByteArray> = Array(3) { ByteArray(numOfPixels) }\n\n        val size: Int = 1 * 2 + 1\n        val total = size * size\n        var r = 0\n        var g = 0\n        var b = 0\n        for (row in 0..<height) {\n            for (col in 0..<width) {\n\n                // 统计滤波器\n                val subpixels = Array(3) { IntArray(total) }\n                var index = 0\n                for (i in -1..1) {\n                    var roffset: Int = row + i\n                    roffset = if (roffset < 0) 0 else (if (roffset >= height) height - 1 else roffset)\n                    for (j in -1..1) {\n                        var coffset: Int = col + j\n                        coffset = if (coffset < 0) 0 else (if (coffset >= width) width - 1 else coffset)\n                        subpixels[0][index] = R[roffset * width + coffset].toInt() and 0xff\n                        subpixels[1][index] = G[roffset * width + coffset].toInt() and 0xff\n                        subpixels[2][index] = B[roffset * width + coffset].toInt() and 0xff\n                        index++\n                    }\n                }\n\n                Arrays.sort(subpixels[0])\n                Arrays.sort(subpixels[1])\n                Arrays.sort(subpixels[2])\n\n                r = subpixels[0][0]\n                g = subpixels[1][0]\n                b = subpixels[2][0]\n\n                output[0][row * width + col] = r.toByte()\n                output[1][row * width + col] = g.toByte()\n                output[2][row * width + col] = b.toByte()\n            }\n        }\n\n        R = output[0]\n        G = output[1]\n        B = output[2]\n\n        return toBufferedImage(dstImage)\n    }\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/filter/blur/MotionFilter.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.filter.blur\n\nimport cn.netdiscovery.monica.imageprocess.filter.base.BaseFilter\nimport cn.netdiscovery.monica.imageprocess.utils.clamp\nimport java.awt.image.BufferedImage\nimport kotlin.math.PI\nimport kotlin.math.cos\nimport kotlin.math.sin\nimport kotlin.math.sqrt\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.filter.blur.MotionFilter\n * @author: Tony Shen\n * @date: 2024/5/1 10:52\n * @version: V1.0 <描述当前版本功能>\n */\nclass MotionFilter(private val distance:Float = 0f,private val angle:Float = 0f,private val zoom:Float = 0.4f): BaseFilter() {\n\n    override fun doFilter(srcImage: BufferedImage, dstImage: BufferedImage): BufferedImage {\n\n        val outPixels = IntArray(width * height)\n\n        var index = 0\n        val cx = width / 2\n        val cy = height / 2\n\n        // calculate the triangle geometry value\n        val sinAngle = sin(angle / 180.0f * PI).toFloat()\n        val coseAngle = cos(angle / 180.0f * PI).toFloat()\n\n        // calculate the distance, same as box blur\n        val imageRadius = sqrt((cx * cx + cy * cy).toDouble()).toFloat()\n        val maxDistance: Float = distance + imageRadius * zoom\n\n        val iteration = maxDistance.toInt()\n        for (row in 0 until height) {\n            var ta = 0\n            var tr = 0\n            var tg = 0\n            var tb = 0\n            for (col in 0 until width) {\n                var newX = col\n                var count = 0\n                var newY = row\n\n                // iterate the source pixels according to distance\n                var m11 = 0.0f\n                var m22 = 0.0f\n                for (i in 0 until iteration) {\n                    newX = col\n                    newY = row\n\n                    // calculate the operator source pixel\n                    if (distance > 0) {\n                        newY = Math.floor((newY + i * sinAngle).toDouble()).toInt()\n                        newX = Math.floor((newX + i * coseAngle).toDouble()).toInt()\n                    }\n                    val f = i.toFloat() / iteration\n                    if (newX < 0 || newX >= width) {\n                        break\n                    }\n                    if (newY < 0 || newY >= height) {\n                        break\n                    }\n\n                    // scale the pixels\n                    val scale = 1 - zoom * f\n                    m11 = cx - cx * scale\n                    m22 = cy - cy * scale\n                    newY = (newY * scale + m22).toInt()\n                    newX = (newX * scale + m11).toInt()\n\n                    // blur the pixels, here\n                    count++\n                    val rgb = inPixels[newY * width + newX]\n                    ta += rgb shr 24 and 0xff\n                    tr += rgb shr 16 and 0xff\n                    tg += rgb shr 8 and 0xff\n                    tb += rgb and 0xff\n                }\n\n                // fill the destination pixel with final RGB value\n                if (count == 0) {\n                    outPixels[index] = inPixels[index]\n                } else {\n                    ta = clamp((ta / count))\n                    tr = clamp((tr / count))\n                    tg = clamp((tg / count))\n                    tb = clamp((tb / count))\n                    outPixels[index] = ta shl 24 or (tr shl 16) or (tg shl 8) or tb\n                }\n                index++\n            }\n        }\n\n        setRGB(dstImage, 0, 0, width, height, outPixels)\n        return dstImage\n    }\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/filter/blur/VariableBlurFilter.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.filter.blur\n\nimport cn.netdiscovery.monica.imageprocess.filter.base.BaseFilter\nimport cn.netdiscovery.monica.imageprocess.utils.premultiply\nimport cn.netdiscovery.monica.imageprocess.utils.unpremultiply\nimport java.awt.image.BufferedImage\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.filter.blur.VariableBlurFilter\n * @author: Tony Shen\n * @date:  2024/5/4 15:04\n * @version: V1.0 <描述当前版本功能>\n */\nclass VariableBlurFilter(private val hRadius: Int =5, private val vRadius:Int=5, private val iterations:Int=1, private val premultiplyAlpha: Boolean = true): BaseFilter() {\n\n    private var blurMask: BufferedImage? = null\n        set(value) {\n            field = value\n        }\n\n    override fun doFilter(srcImage: BufferedImage, dstImage: BufferedImage): BufferedImage {\n        val outPixels = IntArray(width * height)\n\n        if (premultiplyAlpha) premultiply(inPixels, 0, inPixels.size)\n\n        for (i in 0 until iterations) {\n            blur(inPixels, outPixels, width, height, hRadius, 1)\n            blur(outPixels, inPixels, height, width, vRadius, 2)\n        }\n\n        if (premultiplyAlpha) unpremultiply(inPixels, 0, inPixels.size)\n\n        setRGB(dstImage, 0, 0, width, height, inPixels)\n        return dstImage\n    }\n\n    fun blur(`in`: IntArray, out: IntArray, width: Int, height: Int, radius: Int, pass: Int) {\n        val widthMinus1 = width - 1\n        val r = IntArray(width)\n        val g = IntArray(width)\n        val b = IntArray(width)\n        val a = IntArray(width)\n        val mask = IntArray(width)\n\n        var inIndex = 0\n\n        for (y in 0 until height) {\n            var outIndex = y\n\n            if (blurMask != null) {\n                if (pass == 1) getRGB(blurMask!!, 0, y, width, 1, mask)\n                else getRGB(blurMask!!, y, 0, 1, width, mask)\n            }\n\n            for (x in 0 until width) {\n                val argb = `in`[inIndex + x]\n                a[x] = (argb shr 24) and 0xff\n                r[x] = (argb shr 16) and 0xff\n                g[x] = (argb shr 8) and 0xff\n                b[x] = argb and 0xff\n                if (x != 0) {\n                    a[x] += a[x - 1]\n                    r[x] += r[x - 1]\n                    g[x] += g[x - 1]\n                    b[x] += b[x - 1]\n                }\n            }\n\n            for (x in 0 until width) {\n                // Get the blur radius at x, y\n                var ra = if (blurMask != null) {\n                    if (pass == 1) ((mask[x] and 0xff) * hRadius / 255f).toInt()\n                    else ((mask[x] and 0xff) * vRadius / 255f).toInt()\n                } else {\n                    if (pass == 1) (blurRadiusAt(x, y, width, height) * hRadius).toInt()\n                    else (blurRadiusAt(y, x, height, width) * vRadius).toInt()\n                }\n\n                val divisor = 2 * ra + 1\n                var ta = 0\n                var tr = 0\n                var tg = 0\n                var tb = 0\n                var i1 = x + ra\n                if (i1 > widthMinus1) {\n                    val f = i1 - widthMinus1\n                    val l = widthMinus1\n                    ta += (a[l] - a[l - 1]) * f\n                    tr += (r[l] - r[l - 1]) * f\n                    tg += (g[l] - g[l - 1]) * f\n                    tb += (b[l] - b[l - 1]) * f\n                    i1 = widthMinus1\n                }\n                var i2 = x - ra - 1\n                if (i2 < 0) {\n                    ta -= a[0] * i2\n                    tr -= r[0] * i2\n                    tg -= g[0] * i2\n                    tb -= b[0] * i2\n                    i2 = 0\n                }\n\n                ta += a[i1] - a[i2]\n                tr += r[i1] - r[i2]\n                tg += g[i1] - g[i2]\n                tb += b[i1] - b[i2]\n                out[outIndex] =\n                    ((ta / divisor) shl 24) or ((tr / divisor) shl 16) or ((tg / divisor) shl 8) or (tb / divisor)\n\n                outIndex += height\n            }\n            inIndex += width\n        }\n    }\n\n    /**\n     * Override this to get a different blur radius at eahc point.\n     * @param x the x coordinate\n     * @param y the y coordinate\n     * @param width the width of the image\n     * @param height the height of the image\n     * @return the blur radius\n     */\n    private fun blurRadiusAt(x: Int, y: Int, width: Int, height: Int): Float {\n        return x.toFloat() / width\n    }\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/filter/sharpen/LaplaceSharpenFilter.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.filter.sharpen\n\nimport cn.netdiscovery.monica.imageprocess.filter.base.ConvolveFilter\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.filter.sharpen.LaplaceSharpenFilter\n * @author: Tony Shen\n * @date: 2024/5/5 21:05\n * @version: V1.0 <描述当前版本功能>\n */\nclass LaplaceSharpenFilter: ConvolveFilter(sharpenMatrix) {\n\n    companion object {\n        private val sharpenMatrix = floatArrayOf(\n            -1f, -1f,  -1f,\n            -1f,  9f, -1f,\n            -1f, -1f,  -1f,\n        )\n    }\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/filter/sharpen/SharpenFilter.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.filter.sharpen\n\nimport cn.netdiscovery.monica.imageprocess.filter.base.ConvolveFilter\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.filter.sharpen.SharpenFilter\n * @author: Tony Shen\n * @date: 2024/5/5 20:56\n * @version: V1.0 <描述当前版本功能>\n */\nclass SharpenFilter: ConvolveFilter(sharpenMatrix) {\n\n    companion object {\n        private val sharpenMatrix = floatArrayOf(\n            0.0f, -0.2f,  0.0f,\n            -0.2f,  1.8f, -0.2f,\n            0.0f, -0.2f,  0.0f\n        )\n    }\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/filter/sharpen/USMFilter.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.filter.sharpen\n\nimport cn.netdiscovery.monica.imageprocess.filter.blur.GaussianFilter\nimport cn.netdiscovery.monica.imageprocess.utils.clamp\nimport java.awt.image.BufferedImage\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.filter.sharpen.USMFilter\n * @author: Tony Shen\n * @date: 2024/5/1 11:17\n * @version: V1.0 <描述当前版本功能>\n */\nclass USMFilter(override val radius: Float =2f, private val amount: Float = 0.5f, private val threshold:Int =1) :\n    GaussianFilter(radius) {\n\n    override fun doFilter(srcImage: BufferedImage, dstImage: BufferedImage): BufferedImage {\n        val outPixels = IntArray(width * height)\n\n        if ( radius > 0 ) {\n            convolveAndTranspose(kernel, inPixels, outPixels, width, height, alpha, alpha && premultiplyAlpha, false, CLAMP_EDGES)\n            convolveAndTranspose(kernel, outPixels, inPixels, height, width, alpha, false, alpha && premultiplyAlpha, CLAMP_EDGES)\n        }\n\n        getRGB( srcImage,0, 0, width, height, outPixels)\n\n        val a: Float = 4 * amount\n\n        var index = 0\n        for (y in 0 until height) {\n            for (x in 0 until width) {\n                val rgb1 = outPixels[index]\n                var r1 = rgb1 shr 16 and 0xff\n                var g1 = rgb1 shr 8 and 0xff\n                var b1 = rgb1 and 0xff\n\n                val rgb2 = inPixels[index]\n                val r2 = rgb2 shr 16 and 0xff\n                val g2 = rgb2 shr 8 and 0xff\n                val b2 = rgb2 and 0xff\n\n                if (Math.abs(r1 - r2) >= threshold)\n                    r1 = clamp(((a + 1) * (r1 - r2) + r2).toInt())\n                if (Math.abs(g1 - g2) >= threshold)\n                    g1 = clamp(((a + 1) * (g1 - g2) + g2).toInt())\n                if (Math.abs(b1 - b2) >= threshold)\n                    b1 = clamp(((a + 1) * (b1 - b2) + b2).toInt())\n\n                inPixels[index] = rgb1 and 0xff000000.toInt() or (r1 shl 16) or (g1 shl 8) or b1\n                index++\n            }\n        }\n\n        setRGB(dstImage, 0, 0, width, height, inPixels)\n        return dstImage\n    }\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/lut/AutumnLUT.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.lut\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.lut.AutumnLUT\n * @author: Tony Shen\n * @date: 2024/6/17 14:05\n * @version: V1.0 <描述当前版本功能>\n */\n\nobject AutumnLUT {\n    var AUTUMN_LUT = arrayOf(\n        intArrayOf(254, 0, 0),\n        intArrayOf(255, 1, 1),\n        intArrayOf(255, 1, 0),\n        intArrayOf(255, 2, 0),\n        intArrayOf(255, 5, 0),\n        intArrayOf(255, 5, 0),\n        intArrayOf(255, 7, 0),\n        intArrayOf(255, 7, 0),\n        intArrayOf(255, 8, 1),\n        intArrayOf(255, 9, 2),\n        intArrayOf(254, 10, 0),\n        intArrayOf(255, 11, 1),\n        intArrayOf(254, 12, 0),\n        intArrayOf(255, 13, 1),\n        intArrayOf(255, 13, 0),\n        intArrayOf(255, 15, 0),\n        intArrayOf(255, 16, 0),\n        intArrayOf(254, 17, 0),\n        intArrayOf(255, 18, 0),\n        intArrayOf(255, 18, 0),\n        intArrayOf(255, 20, 0),\n        intArrayOf(255, 21, 0),\n        intArrayOf(254, 22, 0),\n        intArrayOf(255, 23, 1),\n        intArrayOf(254, 24, 0),\n        intArrayOf(255, 25, 1),\n        intArrayOf(255, 27, 0),\n        intArrayOf(255, 27, 0),\n        intArrayOf(255, 27, 0),\n        intArrayOf(255, 28, 0),\n        intArrayOf(255, 30, 0),\n        intArrayOf(255, 30, 0),\n        intArrayOf(255, 32, 0),\n        intArrayOf(255, 33, 0),\n        intArrayOf(254, 34, 0),\n        intArrayOf(254, 34, 0),\n        intArrayOf(255, 35, 0),\n        intArrayOf(255, 36, 1),\n        intArrayOf(255, 38, 0),\n        intArrayOf(255, 39, 0),\n        intArrayOf(255, 40, 0),\n        intArrayOf(254, 41, 0),\n        intArrayOf(254, 43, 0),\n        intArrayOf(254, 43, 0),\n        intArrayOf(255, 44, 0),\n        intArrayOf(255, 45, 0),\n        intArrayOf(254, 46, 0),\n        intArrayOf(255, 47, 1),\n        intArrayOf(254, 48, 0),\n        intArrayOf(255, 49, 0),\n        intArrayOf(255, 50, 0),\n        intArrayOf(255, 50, 0),\n        intArrayOf(255, 52, 0),\n        intArrayOf(255, 52, 0),\n        intArrayOf(255, 54, 0),\n        intArrayOf(255, 55, 0),\n        intArrayOf(255, 56, 1),\n        intArrayOf(255, 57, 2),\n        intArrayOf(254, 58, 0),\n        intArrayOf(254, 58, 0),\n        intArrayOf(254, 60, 0),\n        intArrayOf(255, 61, 0),\n        intArrayOf(255, 62, 0),\n        intArrayOf(255, 63, 0),\n        intArrayOf(255, 64, 0),\n        intArrayOf(254, 65, 0),\n        intArrayOf(255, 66, 0),\n        intArrayOf(255, 67, 1),\n        intArrayOf(255, 68, 0),\n        intArrayOf(255, 68, 0),\n        intArrayOf(254, 70, 0),\n        intArrayOf(255, 71, 1),\n        intArrayOf(255, 73, 0),\n        intArrayOf(255, 73, 0),\n        intArrayOf(255, 75, 0),\n        intArrayOf(255, 75, 0),\n        intArrayOf(255, 76, 0),\n        intArrayOf(255, 76, 0),\n        intArrayOf(255, 78, 0),\n        intArrayOf(255, 79, 1),\n        intArrayOf(255, 80, 0),\n        intArrayOf(255, 81, 0),\n        intArrayOf(254, 82, 0),\n        intArrayOf(254, 83, 1),\n        intArrayOf(255, 85, 0),\n        intArrayOf(254, 85, 0),\n        intArrayOf(255, 87, 0),\n        intArrayOf(255, 87, 0),\n        intArrayOf(255, 88, 0),\n        intArrayOf(255, 88, 0),\n        intArrayOf(255, 90, 0),\n        intArrayOf(255, 91, 0),\n        intArrayOf(255, 93, 0),\n        intArrayOf(255, 93, 0),\n        intArrayOf(254, 94, 0),\n        intArrayOf(255, 95, 1),\n        intArrayOf(255, 97, 0),\n        intArrayOf(255, 97, 0),\n        intArrayOf(255, 98, 1),\n        intArrayOf(255, 98, 1),\n        intArrayOf(254, 100, 0),\n        intArrayOf(255, 101, 1),\n        intArrayOf(255, 102, 0),\n        intArrayOf(255, 103, 1),\n        intArrayOf(255, 103, 0),\n        intArrayOf(255, 104, 0),\n        intArrayOf(255, 105, 0),\n        intArrayOf(255, 107, 0),\n        intArrayOf(255, 108, 0),\n        intArrayOf(255, 109, 0),\n        intArrayOf(255, 110, 1),\n        intArrayOf(255, 110, 1),\n        intArrayOf(254, 112, 0),\n        intArrayOf(255, 113, 1),\n        intArrayOf(255, 115, 0),\n        intArrayOf(255, 115, 0),\n        intArrayOf(255, 116, 0),\n        intArrayOf(255, 117, 0),\n        intArrayOf(255, 119, 0),\n        intArrayOf(255, 119, 0),\n        intArrayOf(255, 120, 2),\n        intArrayOf(255, 120, 2),\n        intArrayOf(255, 122, 1),\n        intArrayOf(255, 122, 1),\n        intArrayOf(254, 124, 0),\n        intArrayOf(255, 125, 1),\n        intArrayOf(255, 126, 0),\n        intArrayOf(255, 127, 0),\n        intArrayOf(255, 128, 0),\n        intArrayOf(254, 129, 0),\n        intArrayOf(255, 130, 1),\n        intArrayOf(255, 130, 1),\n        intArrayOf(255, 133, 0),\n        intArrayOf(255, 133, 0),\n        intArrayOf(255, 134, 1),\n        intArrayOf(255, 134, 1),\n        intArrayOf(254, 136, 0),\n        intArrayOf(255, 137, 1),\n        intArrayOf(255, 139, 0),\n        intArrayOf(255, 139, 0),\n        intArrayOf(255, 140, 0),\n        intArrayOf(255, 141, 0),\n        intArrayOf(255, 142, 1),\n        intArrayOf(255, 142, 1),\n        intArrayOf(255, 144, 0),\n        intArrayOf(255, 144, 0),\n        intArrayOf(255, 146, 1),\n        intArrayOf(255, 147, 2),\n        intArrayOf(255, 149, 1),\n        intArrayOf(255, 149, 1),\n        intArrayOf(255, 150, 0),\n        intArrayOf(255, 151, 0),\n        intArrayOf(255, 152, 0),\n        intArrayOf(254, 153, 0),\n        intArrayOf(254, 155, 0),\n        intArrayOf(254, 155, 0),\n        intArrayOf(255, 156, 0),\n        intArrayOf(255, 156, 0),\n        intArrayOf(255, 158, 1),\n        intArrayOf(255, 159, 2),\n        intArrayOf(254, 160, 0),\n        intArrayOf(255, 161, 1),\n        intArrayOf(255, 162, 0),\n        intArrayOf(255, 163, 0),\n        intArrayOf(255, 165, 0),\n        intArrayOf(255, 165, 0),\n        intArrayOf(255, 167, 0),\n        intArrayOf(255, 167, 0),\n        intArrayOf(255, 168, 1),\n        intArrayOf(255, 168, 1),\n        intArrayOf(255, 170, 1),\n        intArrayOf(255, 171, 2),\n        intArrayOf(255, 173, 1),\n        intArrayOf(255, 173, 1),\n        intArrayOf(255, 173, 0),\n        intArrayOf(255, 175, 0),\n        intArrayOf(255, 177, 0),\n        intArrayOf(254, 177, 0),\n        intArrayOf(255, 178, 0),\n        intArrayOf(255, 179, 1),\n        intArrayOf(255, 180, 0),\n        intArrayOf(255, 180, 0),\n        intArrayOf(255, 182, 0),\n        intArrayOf(255, 183, 1),\n        intArrayOf(254, 184, 0),\n        intArrayOf(255, 185, 1),\n        intArrayOf(254, 186, 0),\n        intArrayOf(255, 187, 0),\n        intArrayOf(255, 188, 0),\n        intArrayOf(255, 188, 0),\n        intArrayOf(255, 190, 0),\n        intArrayOf(255, 190, 0),\n        intArrayOf(255, 192, 0),\n        intArrayOf(255, 193, 0),\n        intArrayOf(254, 194, 0),\n        intArrayOf(255, 195, 1),\n        intArrayOf(255, 195, 0),\n        intArrayOf(255, 196, 1),\n        intArrayOf(255, 197, 0),\n        intArrayOf(255, 199, 0),\n        intArrayOf(255, 200, 0),\n        intArrayOf(254, 201, 0),\n        intArrayOf(254, 203, 0),\n        intArrayOf(254, 203, 0),\n        intArrayOf(255, 204, 0),\n        intArrayOf(255, 205, 0),\n        intArrayOf(254, 206, 0),\n        intArrayOf(255, 207, 1),\n        intArrayOf(254, 208, 0),\n        intArrayOf(255, 209, 1),\n        intArrayOf(255, 210, 0),\n        intArrayOf(255, 210, 0),\n        intArrayOf(255, 212, 0),\n        intArrayOf(255, 212, 0),\n        intArrayOf(255, 214, 0),\n        intArrayOf(255, 214, 0),\n        intArrayOf(255, 216, 1),\n        intArrayOf(255, 217, 2),\n        intArrayOf(255, 219, 1),\n        intArrayOf(255, 219, 1),\n        intArrayOf(254, 220, 0),\n        intArrayOf(255, 221, 0),\n        intArrayOf(255, 222, 0),\n        intArrayOf(255, 223, 0),\n        intArrayOf(255, 224, 0),\n        intArrayOf(254, 225, 0),\n        intArrayOf(255, 226, 0),\n        intArrayOf(255, 226, 0),\n        intArrayOf(255, 228, 0),\n        intArrayOf(255, 229, 0),\n        intArrayOf(254, 230, 0),\n        intArrayOf(255, 231, 1),\n        intArrayOf(254, 232, 0),\n        intArrayOf(255, 233, 0),\n        intArrayOf(255, 235, 0),\n        intArrayOf(255, 235, 0),\n        intArrayOf(255, 236, 0),\n        intArrayOf(255, 236, 0),\n        intArrayOf(255, 238, 0),\n        intArrayOf(255, 239, 1),\n        intArrayOf(255, 240, 0),\n        intArrayOf(255, 241, 0),\n        intArrayOf(255, 243, 1),\n        intArrayOf(255, 243, 1),\n        intArrayOf(255, 243, 0),\n        intArrayOf(255, 244, 0),\n        intArrayOf(255, 246, 0),\n        intArrayOf(255, 247, 0),\n        intArrayOf(255, 248, 0),\n        intArrayOf(254, 249, 0),\n        intArrayOf(254, 251, 0),\n        intArrayOf(255, 252, 0),\n        intArrayOf(255, 253, 0),\n        intArrayOf(255, 253, 0),\n        intArrayOf(254, 254, 0),\n        intArrayOf(255, 255, 1)\n    )\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/lut/BoneLUT.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.lut\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.lut.BoneLUT\n * @author: Tony Shen\n * @date: 2024/6/17 14:07\n * @version: V1.0 <描述当前版本功能>\n */\n\nobject BoneLUT {\n    var BONE_LUT = arrayOf(\n        intArrayOf(0, 0, 0),\n        intArrayOf(1, 1, 1),\n        intArrayOf(2, 2, 2),\n        intArrayOf(3, 3, 3),\n        intArrayOf(4, 4, 4),\n        intArrayOf(4, 4, 4),\n        intArrayOf(5, 5, 7),\n        intArrayOf(6, 6, 8),\n        intArrayOf(7, 7, 9),\n        intArrayOf(8, 8, 10),\n        intArrayOf(9, 8, 13),\n        intArrayOf(10, 9, 14),\n        intArrayOf(11, 10, 15),\n        intArrayOf(12, 11, 16),\n        intArrayOf(13, 12, 17),\n        intArrayOf(14, 13, 18),\n        intArrayOf(15, 14, 19),\n        intArrayOf(16, 15, 20),\n        intArrayOf(17, 16, 22),\n        intArrayOf(18, 17, 23),\n        intArrayOf(18, 19, 24),\n        intArrayOf(18, 19, 24),\n        intArrayOf(19, 19, 27),\n        intArrayOf(20, 20, 28),\n        intArrayOf(21, 21, 29),\n        intArrayOf(22, 22, 30),\n        intArrayOf(23, 23, 31),\n        intArrayOf(24, 24, 32),\n        intArrayOf(25, 25, 33),\n        intArrayOf(25, 25, 33),\n        intArrayOf(26, 26, 34),\n        intArrayOf(27, 27, 35),\n        intArrayOf(28, 28, 38),\n        intArrayOf(29, 29, 39),\n        intArrayOf(30, 30, 40),\n        intArrayOf(31, 31, 41),\n        intArrayOf(32, 32, 42),\n        intArrayOf(32, 32, 42),\n        intArrayOf(33, 33, 45),\n        intArrayOf(34, 34, 46),\n        intArrayOf(35, 35, 47),\n        intArrayOf(37, 37, 49),\n        intArrayOf(38, 37, 51),\n        intArrayOf(39, 38, 52),\n        intArrayOf(40, 39, 55),\n        intArrayOf(40, 39, 55),\n        intArrayOf(41, 40, 56),\n        intArrayOf(42, 41, 57),\n        intArrayOf(43, 42, 58),\n        intArrayOf(44, 43, 59),\n        intArrayOf(45, 44, 60),\n        intArrayOf(46, 45, 61),\n        intArrayOf(47, 46, 62),\n        intArrayOf(47, 46, 62),\n        intArrayOf(48, 47, 65),\n        intArrayOf(49, 48, 66),\n        intArrayOf(48, 49, 67),\n        intArrayOf(49, 50, 68),\n        intArrayOf(50, 51, 71),\n        intArrayOf(51, 52, 72),\n        intArrayOf(52, 53, 73),\n        intArrayOf(52, 53, 73),\n        intArrayOf(53, 54, 74),\n        intArrayOf(54, 55, 75),\n        intArrayOf(55, 56, 76),\n        intArrayOf(57, 58, 78),\n        intArrayOf(58, 59, 80),\n        intArrayOf(59, 60, 81),\n        intArrayOf(60, 61, 82),\n        intArrayOf(60, 61, 82),\n        intArrayOf(61, 61, 85),\n        intArrayOf(62, 62, 86),\n        intArrayOf(63, 63, 87),\n        intArrayOf(64, 64, 88),\n        intArrayOf(65, 65, 89),\n        intArrayOf(66, 66, 90),\n        intArrayOf(67, 67, 91),\n        intArrayOf(67, 67, 91),\n        intArrayOf(68, 68, 92),\n        intArrayOf(69, 69, 93),\n        intArrayOf(70, 70, 96),\n        intArrayOf(71, 71, 97),\n        intArrayOf(72, 72, 98),\n        intArrayOf(73, 73, 99),\n        intArrayOf(73, 73, 99),\n        intArrayOf(74, 74, 100),\n        intArrayOf(75, 75, 103),\n        intArrayOf(76, 76, 104),\n        intArrayOf(77, 76, 107),\n        intArrayOf(78, 77, 108),\n        intArrayOf(79, 78, 109),\n        intArrayOf(80, 79, 110),\n        intArrayOf(80, 81, 111),\n        intArrayOf(81, 82, 112),\n        intArrayOf(82, 83, 114),\n        intArrayOf(83, 84, 115),\n        intArrayOf(84, 85, 116),\n        intArrayOf(85, 86, 117),\n        intArrayOf(86, 87, 118),\n        intArrayOf(87, 88, 119),\n        intArrayOf(88, 89, 120),\n        intArrayOf(89, 90, 121),\n        intArrayOf(90, 91, 121),\n        intArrayOf(91, 92, 122),\n        intArrayOf(90, 94, 123),\n        intArrayOf(91, 95, 124),\n        intArrayOf(92, 96, 123),\n        intArrayOf(93, 97, 124),\n        intArrayOf(94, 100, 126),\n        intArrayOf(95, 101, 127),\n        intArrayOf(96, 102, 128),\n        intArrayOf(97, 103, 129),\n        intArrayOf(98, 104, 130),\n        intArrayOf(99, 105, 131),\n        intArrayOf(100, 106, 132),\n        intArrayOf(101, 107, 133),\n        intArrayOf(102, 108, 132),\n        intArrayOf(103, 109, 133),\n        intArrayOf(103, 111, 134),\n        intArrayOf(104, 112, 135),\n        intArrayOf(105, 113, 136),\n        intArrayOf(106, 114, 137),\n        intArrayOf(106, 117, 139),\n        intArrayOf(107, 118, 140),\n        intArrayOf(108, 119, 141),\n        intArrayOf(109, 120, 142),\n        intArrayOf(110, 121, 143),\n        intArrayOf(111, 122, 144),\n        intArrayOf(112, 123, 143),\n        intArrayOf(112, 123, 143),\n        intArrayOf(114, 125, 145),\n        intArrayOf(115, 126, 146),\n        intArrayOf(116, 127, 147),\n        intArrayOf(117, 128, 148),\n        intArrayOf(117, 130, 149),\n        intArrayOf(118, 131, 150),\n        intArrayOf(119, 132, 149),\n        intArrayOf(121, 134, 151),\n        intArrayOf(120, 136, 152),\n        intArrayOf(121, 137, 153),\n        intArrayOf(122, 138, 154),\n        intArrayOf(122, 138, 154),\n        intArrayOf(124, 140, 156),\n        intArrayOf(125, 141, 157),\n        intArrayOf(125, 142, 158),\n        intArrayOf(126, 143, 159),\n        intArrayOf(128, 145, 161),\n        intArrayOf(129, 146, 162),\n        intArrayOf(129, 147, 161),\n        intArrayOf(130, 148, 162),\n        intArrayOf(131, 149, 163),\n        intArrayOf(133, 151, 165),\n        intArrayOf(134, 152, 166),\n        intArrayOf(135, 153, 167),\n        intArrayOf(135, 154, 168),\n        intArrayOf(136, 155, 169),\n        intArrayOf(137, 157, 168),\n        intArrayOf(138, 158, 169),\n        intArrayOf(139, 159, 170),\n        intArrayOf(140, 160, 171),\n        intArrayOf(139, 161, 172),\n        intArrayOf(141, 163, 174),\n        intArrayOf(142, 164, 175),\n        intArrayOf(142, 164, 175),\n        intArrayOf(143, 165, 176),\n        intArrayOf(144, 166, 177),\n        intArrayOf(145, 170, 177),\n        intArrayOf(146, 171, 178),\n        intArrayOf(147, 172, 179),\n        intArrayOf(148, 173, 180),\n        intArrayOf(149, 174, 179),\n        intArrayOf(150, 175, 180),\n        intArrayOf(151, 176, 181),\n        intArrayOf(152, 177, 182),\n        intArrayOf(153, 178, 183),\n        intArrayOf(154, 179, 184),\n        intArrayOf(153, 181, 185),\n        intArrayOf(154, 182, 186),\n        intArrayOf(155, 183, 187),\n        intArrayOf(156, 184, 188),\n        intArrayOf(158, 186, 190),\n        intArrayOf(159, 187, 191),\n        intArrayOf(159, 189, 191),\n        intArrayOf(160, 190, 192),\n        intArrayOf(161, 191, 193),\n        intArrayOf(162, 192, 194),\n        intArrayOf(162, 193, 195),\n        intArrayOf(163, 194, 196),\n        intArrayOf(164, 195, 197),\n        intArrayOf(165, 196, 198),\n        intArrayOf(166, 197, 199),\n        intArrayOf(167, 198, 200),\n        intArrayOf(169, 201, 200),\n        intArrayOf(170, 202, 201),\n        intArrayOf(171, 203, 202),\n        intArrayOf(172, 204, 203),\n        intArrayOf(173, 203, 203),\n        intArrayOf(174, 204, 204),\n        intArrayOf(176, 206, 206),\n        intArrayOf(177, 207, 207),\n        intArrayOf(179, 207, 208),\n        intArrayOf(180, 208, 209),\n        intArrayOf(183, 209, 210),\n        intArrayOf(184, 210, 211),\n        intArrayOf(185, 211, 210),\n        intArrayOf(186, 212, 211),\n        intArrayOf(188, 214, 213),\n        intArrayOf(188, 214, 213),\n        intArrayOf(190, 214, 216),\n        intArrayOf(191, 215, 217),\n        intArrayOf(194, 215, 216),\n        intArrayOf(195, 216, 217),\n        intArrayOf(196, 217, 218),\n        intArrayOf(197, 218, 219),\n        intArrayOf(199, 219, 218),\n        intArrayOf(200, 220, 219),\n        intArrayOf(201, 221, 220),\n        intArrayOf(202, 222, 221),\n        intArrayOf(204, 222, 222),\n        intArrayOf(205, 223, 223),\n        intArrayOf(206, 224, 224),\n        intArrayOf(207, 225, 225),\n        intArrayOf(210, 226, 226),\n        intArrayOf(211, 227, 227),\n        intArrayOf(210, 228, 228),\n        intArrayOf(212, 230, 230),\n        intArrayOf(215, 231, 231),\n        intArrayOf(216, 232, 232),\n        intArrayOf(219, 230, 232),\n        intArrayOf(220, 231, 233),\n        intArrayOf(221, 233, 233),\n        intArrayOf(222, 234, 234),\n        intArrayOf(223, 235, 235),\n        intArrayOf(225, 237, 237),\n        intArrayOf(227, 239, 239),\n        intArrayOf(227, 239, 239),\n        intArrayOf(228, 238, 239),\n        intArrayOf(229, 239, 240),\n        intArrayOf(232, 240, 242),\n        intArrayOf(233, 241, 243),\n        intArrayOf(234, 243, 242),\n        intArrayOf(235, 244, 243),\n        intArrayOf(238, 244, 244),\n        intArrayOf(239, 245, 245),\n        intArrayOf(240, 246, 246),\n        intArrayOf(241, 247, 247),\n        intArrayOf(243, 247, 248),\n        intArrayOf(244, 248, 249),\n        intArrayOf(245, 249, 250),\n        intArrayOf(246, 250, 251),\n        intArrayOf(249, 250, 252),\n        intArrayOf(250, 251, 253),\n        intArrayOf(251, 253, 252),\n        intArrayOf(252, 254, 253),\n        intArrayOf(254, 254, 254),\n        intArrayOf(255, 255, 255)\n    )\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/lut/CoolLUT.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.lut\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.lut.CoolLUT\n * @author: Tony Shen\n * @date: 2024/6/17 14:10\n * @version: V1.0 <描述当前版本功能>\n */\n\nobject CoolLUT {\n    var COOL_LUT = arrayOf(\n        intArrayOf(0, 255, 255),\n        intArrayOf(0, 254, 255),\n        intArrayOf(1, 253, 255),\n        intArrayOf(3, 252, 255),\n        intArrayOf(4, 251, 255),\n        intArrayOf(5, 250, 255),\n        intArrayOf(7, 248, 255),\n        intArrayOf(7, 248, 255),\n        intArrayOf(8, 247, 254),\n        intArrayOf(8, 247, 254),\n        intArrayOf(11, 245, 255),\n        intArrayOf(11, 245, 255),\n        intArrayOf(12, 242, 255),\n        intArrayOf(12, 242, 255),\n        intArrayOf(15, 241, 255),\n        intArrayOf(14, 240, 254),\n        intArrayOf(16, 239, 255),\n        intArrayOf(16, 239, 255),\n        intArrayOf(18, 237, 255),\n        intArrayOf(19, 236, 255),\n        intArrayOf(20, 235, 255),\n        intArrayOf(22, 234, 255),\n        intArrayOf(24, 233, 255),\n        intArrayOf(23, 232, 255),\n        intArrayOf(25, 231, 255),\n        intArrayOf(25, 231, 255),\n        intArrayOf(27, 228, 255),\n        intArrayOf(27, 228, 255),\n        intArrayOf(29, 227, 255),\n        intArrayOf(28, 226, 255),\n        intArrayOf(30, 225, 255),\n        intArrayOf(30, 225, 255),\n        intArrayOf(33, 223, 255),\n        intArrayOf(32, 222, 254),\n        intArrayOf(34, 221, 255),\n        intArrayOf(34, 221, 255),\n        intArrayOf(37, 219, 255),\n        intArrayOf(36, 218, 255),\n        intArrayOf(39, 217, 255),\n        intArrayOf(39, 217, 255),\n        intArrayOf(39, 215, 255),\n        intArrayOf(39, 215, 255),\n        intArrayOf(42, 213, 255),\n        intArrayOf(43, 212, 255),\n        intArrayOf(44, 211, 255),\n        intArrayOf(45, 210, 255),\n        intArrayOf(48, 208, 255),\n        intArrayOf(47, 207, 255),\n        intArrayOf(50, 206, 255),\n        intArrayOf(50, 206, 255),\n        intArrayOf(50, 204, 254),\n        intArrayOf(50, 204, 254),\n        intArrayOf(52, 204, 254),\n        intArrayOf(51, 203, 253),\n        intArrayOf(54, 201, 255),\n        intArrayOf(55, 200, 255),\n        intArrayOf(56, 199, 255),\n        intArrayOf(57, 198, 254),\n        intArrayOf(60, 196, 255),\n        intArrayOf(60, 196, 255),\n        intArrayOf(61, 195, 255),\n        intArrayOf(60, 194, 255),\n        intArrayOf(61, 193, 255),\n        intArrayOf(61, 193, 255),\n        intArrayOf(63, 191, 254),\n        intArrayOf(63, 191, 254),\n        intArrayOf(66, 189, 255),\n        intArrayOf(66, 188, 255),\n        intArrayOf(68, 187, 255),\n        intArrayOf(69, 186, 255),\n        intArrayOf(72, 184, 255),\n        intArrayOf(71, 183, 255),\n        intArrayOf(72, 183, 255),\n        intArrayOf(72, 183, 255),\n        intArrayOf(74, 180, 254),\n        intArrayOf(74, 180, 254),\n        intArrayOf(77, 179, 254),\n        intArrayOf(77, 179, 254),\n        intArrayOf(77, 177, 253),\n        intArrayOf(77, 177, 253),\n        intArrayOf(80, 175, 255),\n        intArrayOf(79, 174, 255),\n        intArrayOf(82, 173, 255),\n        intArrayOf(82, 173, 255),\n        intArrayOf(85, 171, 255),\n        intArrayOf(84, 170, 255),\n        intArrayOf(87, 169, 255),\n        intArrayOf(87, 169, 255),\n        intArrayOf(87, 167, 254),\n        intArrayOf(87, 167, 254),\n        intArrayOf(90, 165, 255),\n        intArrayOf(91, 164, 255),\n        intArrayOf(92, 163, 255),\n        intArrayOf(93, 162, 255),\n        intArrayOf(96, 161, 255),\n        intArrayOf(95, 160, 254),\n        intArrayOf(98, 158, 255),\n        intArrayOf(98, 158, 255),\n        intArrayOf(99, 157, 255),\n        intArrayOf(98, 156, 255),\n        intArrayOf(100, 155, 255),\n        intArrayOf(100, 155, 255),\n        intArrayOf(102, 154, 255),\n        intArrayOf(103, 152, 255),\n        intArrayOf(104, 151, 255),\n        intArrayOf(106, 150, 255),\n        intArrayOf(107, 148, 255),\n        intArrayOf(107, 148, 255),\n        intArrayOf(109, 147, 255),\n        intArrayOf(108, 146, 255),\n        intArrayOf(109, 145, 255),\n        intArrayOf(109, 145, 255),\n        intArrayOf(111, 143, 254),\n        intArrayOf(111, 143, 254),\n        intArrayOf(114, 141, 255),\n        intArrayOf(115, 140, 255),\n        intArrayOf(116, 139, 255),\n        intArrayOf(117, 138, 255),\n        intArrayOf(120, 137, 255),\n        intArrayOf(119, 136, 254),\n        intArrayOf(120, 134, 255),\n        intArrayOf(120, 134, 255),\n        intArrayOf(123, 133, 255),\n        intArrayOf(122, 132, 255),\n        intArrayOf(125, 131, 255),\n        intArrayOf(125, 131, 255),\n        intArrayOf(126, 130, 255),\n        intArrayOf(125, 129, 255),\n        intArrayOf(128, 127, 255),\n        intArrayOf(128, 127, 255),\n        intArrayOf(130, 125, 254),\n        intArrayOf(131, 124, 254),\n        intArrayOf(133, 123, 254),\n        intArrayOf(133, 122, 253),\n        intArrayOf(134, 120, 255),\n        intArrayOf(134, 120, 255),\n        intArrayOf(137, 119, 255),\n        intArrayOf(136, 118, 254),\n        intArrayOf(138, 117, 255),\n        intArrayOf(139, 116, 255),\n        intArrayOf(140, 116, 255),\n        intArrayOf(141, 114, 255),\n        intArrayOf(144, 112, 255),\n        intArrayOf(144, 112, 255),\n        intArrayOf(144, 111, 254),\n        intArrayOf(144, 111, 254),\n        intArrayOf(147, 109, 255),\n        intArrayOf(146, 108, 255),\n        intArrayOf(149, 107, 255),\n        intArrayOf(149, 107, 255),\n        intArrayOf(151, 105, 255),\n        intArrayOf(150, 104, 255),\n        intArrayOf(152, 103, 255),\n        intArrayOf(152, 103, 255),\n        intArrayOf(154, 101, 254),\n        intArrayOf(155, 100, 254),\n        intArrayOf(156, 99, 254),\n        intArrayOf(158, 98, 254),\n        intArrayOf(160, 96, 253),\n        intArrayOf(160, 96, 253),\n        intArrayOf(160, 95, 255),\n        intArrayOf(159, 94, 255),\n        intArrayOf(161, 93, 255),\n        intArrayOf(163, 92, 255),\n        intArrayOf(164, 91, 255),\n        intArrayOf(165, 90, 255),\n        intArrayOf(167, 88, 255),\n        intArrayOf(167, 88, 255),\n        intArrayOf(169, 86, 254),\n        intArrayOf(169, 86, 254),\n        intArrayOf(171, 85, 255),\n        intArrayOf(171, 85, 255),\n        intArrayOf(172, 82, 255),\n        intArrayOf(172, 82, 255),\n        intArrayOf(175, 81, 255),\n        intArrayOf(174, 80, 254),\n        intArrayOf(176, 79, 255),\n        intArrayOf(176, 79, 255),\n        intArrayOf(177, 77, 255),\n        intArrayOf(179, 76, 255),\n        intArrayOf(180, 75, 255),\n        intArrayOf(182, 74, 255),\n        intArrayOf(183, 72, 255),\n        intArrayOf(183, 72, 255),\n        intArrayOf(185, 71, 255),\n        intArrayOf(184, 70, 254),\n        intArrayOf(187, 68, 255),\n        intArrayOf(187, 68, 255),\n        intArrayOf(189, 67, 255),\n        intArrayOf(188, 66, 255),\n        intArrayOf(191, 64, 255),\n        intArrayOf(191, 64, 255),\n        intArrayOf(193, 62, 254),\n        intArrayOf(193, 62, 254),\n        intArrayOf(195, 61, 255),\n        intArrayOf(194, 60, 255),\n        intArrayOf(196, 58, 255),\n        intArrayOf(196, 58, 255),\n        intArrayOf(199, 57, 255),\n        intArrayOf(198, 56, 254),\n        intArrayOf(200, 55, 255),\n        intArrayOf(200, 55, 255),\n        intArrayOf(202, 53, 255),\n        intArrayOf(203, 52, 255),\n        intArrayOf(204, 51, 255),\n        intArrayOf(206, 50, 255),\n        intArrayOf(207, 48, 255),\n        intArrayOf(207, 48, 255),\n        intArrayOf(210, 46, 255),\n        intArrayOf(209, 45, 254),\n        intArrayOf(211, 44, 254),\n        intArrayOf(211, 44, 254),\n        intArrayOf(212, 44, 254),\n        intArrayOf(211, 43, 253),\n        intArrayOf(214, 41, 255),\n        intArrayOf(215, 40, 255),\n        intArrayOf(217, 39, 255),\n        intArrayOf(217, 38, 254),\n        intArrayOf(220, 36, 255),\n        intArrayOf(220, 36, 255),\n        intArrayOf(220, 34, 255),\n        intArrayOf(220, 34, 255),\n        intArrayOf(222, 34, 255),\n        intArrayOf(222, 34, 255),\n        intArrayOf(223, 31, 254),\n        intArrayOf(223, 31, 254),\n        intArrayOf(226, 29, 255),\n        intArrayOf(227, 28, 255),\n        intArrayOf(228, 27, 255),\n        intArrayOf(229, 26, 255),\n        intArrayOf(232, 24, 255),\n        intArrayOf(231, 23, 255),\n        intArrayOf(233, 23, 255),\n        intArrayOf(232, 22, 255),\n        intArrayOf(234, 20, 255),\n        intArrayOf(234, 20, 255),\n        intArrayOf(237, 19, 255),\n        intArrayOf(236, 18, 254),\n        intArrayOf(239, 16, 254),\n        intArrayOf(239, 16, 254),\n        intArrayOf(242, 14, 255),\n        intArrayOf(241, 13, 255),\n        intArrayOf(242, 13, 255),\n        intArrayOf(242, 13, 255),\n        intArrayOf(244, 10, 255),\n        intArrayOf(244, 10, 255),\n        intArrayOf(247, 9, 255),\n        intArrayOf(247, 9, 255),\n        intArrayOf(247, 7, 254),\n        intArrayOf(247, 7, 254),\n        intArrayOf(250, 5, 255),\n        intArrayOf(250, 4, 255),\n        intArrayOf(252, 3, 255),\n        intArrayOf(253, 2, 255),\n        intArrayOf(255, 1, 255),\n        intArrayOf(255, 0, 254)\n    )\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/lut/HotLUT.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.lut\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.lut.HotLUT\n * @author: Tony Shen\n * @date: 2024/6/17 14:16\n * @version: V1.0 <描述当前版本功能>\n */\n\nobject HotLUT {\n    var HOT_LUT = arrayOf(\n        intArrayOf(0, 0, 0),\n        intArrayOf(1, 1, 0),\n        intArrayOf(4, 0, 0),\n        intArrayOf(6, 0, 0),\n        intArrayOf(9, 1, 0),\n        intArrayOf(12, 0, 0),\n        intArrayOf(14, 0, 0),\n        intArrayOf(16, 0, 0),\n        intArrayOf(20, 0, 0),\n        intArrayOf(22, 1, 0),\n        intArrayOf(24, 0, 0),\n        intArrayOf(28, 0, 0),\n        intArrayOf(30, 0, 0),\n        intArrayOf(32, 0, 1),\n        intArrayOf(35, 0, 0),\n        intArrayOf(36, 0, 0),\n        intArrayOf(40, 0, 0),\n        intArrayOf(41, 1, 1),\n        intArrayOf(44, 0, 0),\n        intArrayOf(46, 0, 0),\n        intArrayOf(49, 1, 1),\n        intArrayOf(52, 0, 2),\n        intArrayOf(54, 0, 0),\n        intArrayOf(56, 0, 0),\n        intArrayOf(60, 0, 0),\n        intArrayOf(62, 1, 0),\n        intArrayOf(64, 0, 0),\n        intArrayOf(68, 0, 0),\n        intArrayOf(70, 0, 0),\n        intArrayOf(72, 0, 1),\n        intArrayOf(75, 0, 0),\n        intArrayOf(76, 0, 0),\n        intArrayOf(80, 0, 0),\n        intArrayOf(81, 1, 0),\n        intArrayOf(84, 0, 0),\n        intArrayOf(86, 0, 0),\n        intArrayOf(89, 1, 0),\n        intArrayOf(92, 0, 1),\n        intArrayOf(94, 0, 0),\n        intArrayOf(96, 0, 1),\n        intArrayOf(100, 0, 0),\n        intArrayOf(103, 0, 1),\n        intArrayOf(104, 0, 0),\n        intArrayOf(108, 0, 0),\n        intArrayOf(110, 0, 0),\n        intArrayOf(113, 1, 0),\n        intArrayOf(115, 0, 0),\n        intArrayOf(116, 0, 0),\n        intArrayOf(120, 0, 1),\n        intArrayOf(120, 0, 0),\n        intArrayOf(124, 0, 0),\n        intArrayOf(126, 0, 1),\n        intArrayOf(129, 1, 0),\n        intArrayOf(132, 0, 0),\n        intArrayOf(134, 0, 0),\n        intArrayOf(136, 0, 0),\n        intArrayOf(140, 0, 0),\n        intArrayOf(142, 0, 0),\n        intArrayOf(144, 0, 0),\n        intArrayOf(148, 0, 0),\n        intArrayOf(150, 0, 0),\n        intArrayOf(152, 0, 0),\n        intArrayOf(155, 0, 0),\n        intArrayOf(156, 0, 1),\n        intArrayOf(160, 0, 0),\n        intArrayOf(160, 0, 0),\n        intArrayOf(164, 0, 0),\n        intArrayOf(166, 1, 0),\n        intArrayOf(169, 1, 0),\n        intArrayOf(172, 0, 0),\n        intArrayOf(174, 1, 0),\n        intArrayOf(176, 1, 0),\n        intArrayOf(180, 0, 0),\n        intArrayOf(182, 0, 0),\n        intArrayOf(184, 0, 0),\n        intArrayOf(188, 0, 1),\n        intArrayOf(190, 0, 0),\n        intArrayOf(192, 0, 0),\n        intArrayOf(195, 0, 0),\n        intArrayOf(196, 0, 1),\n        intArrayOf(200, 0, 0),\n        intArrayOf(200, 0, 0),\n        intArrayOf(204, 0, 0),\n        intArrayOf(206, 0, 0),\n        intArrayOf(209, 1, 0),\n        intArrayOf(212, 0, 0),\n        intArrayOf(214, 0, 0),\n        intArrayOf(216, 1, 0),\n        intArrayOf(220, 0, 0),\n        intArrayOf(222, 0, 0),\n        intArrayOf(224, 0, 1),\n        intArrayOf(228, 0, 1),\n        intArrayOf(230, 0, 2),\n        intArrayOf(232, 0, 0),\n        intArrayOf(235, 0, 0),\n        intArrayOf(236, 0, 0),\n        intArrayOf(240, 0, 0),\n        intArrayOf(242, 0, 0),\n        intArrayOf(244, 0, 0),\n        intArrayOf(246, 0, 1),\n        intArrayOf(250, 0, 1),\n        intArrayOf(250, 0, 1),\n        intArrayOf(254, 2, 1),\n        intArrayOf(255, 3, 0),\n        intArrayOf(254, 5, 1),\n        intArrayOf(255, 8, 0),\n        intArrayOf(254, 10, 0),\n        intArrayOf(254, 14, 0),\n        intArrayOf(255, 15, 0),\n        intArrayOf(255, 18, 0),\n        intArrayOf(255, 20, 0),\n        intArrayOf(255, 23, 0),\n        intArrayOf(255, 25, 1),\n        intArrayOf(254, 29, 0),\n        intArrayOf(255, 30, 0),\n        intArrayOf(253, 33, 0),\n        intArrayOf(255, 35, 0),\n        intArrayOf(255, 39, 0),\n        intArrayOf(255, 40, 0),\n        intArrayOf(254, 43, 0),\n        intArrayOf(255, 45, 0),\n        intArrayOf(254, 48, 0),\n        intArrayOf(255, 49, 0),\n        intArrayOf(254, 53, 0),\n        intArrayOf(255, 55, 1),\n        intArrayOf(254, 58, 0),\n        intArrayOf(255, 59, 0),\n        intArrayOf(255, 63, 0),\n        intArrayOf(254, 65, 0),\n        intArrayOf(255, 68, 0),\n        intArrayOf(254, 70, 0),\n        intArrayOf(255, 73, 0),\n        intArrayOf(255, 74, 0),\n        intArrayOf(255, 78, 0),\n        intArrayOf(255, 79, 1),\n        intArrayOf(255, 83, 0),\n        intArrayOf(255, 84, 0),\n        intArrayOf(255, 88, 0),\n        intArrayOf(255, 89, 0),\n        intArrayOf(255, 93, 0),\n        intArrayOf(255, 95, 0),\n        intArrayOf(255, 98, 1),\n        intArrayOf(255, 100, 0),\n        intArrayOf(255, 104, 1),\n        intArrayOf(255, 105, 0),\n        intArrayOf(255, 109, 0),\n        intArrayOf(255, 111, 0),\n        intArrayOf(255, 113, 1),\n        intArrayOf(255, 115, 0),\n        intArrayOf(254, 119, 1),\n        intArrayOf(255, 120, 2),\n        intArrayOf(255, 123, 0),\n        intArrayOf(255, 125, 1),\n        intArrayOf(255, 128, 0),\n        intArrayOf(255, 130, 1),\n        intArrayOf(255, 133, 0),\n        intArrayOf(255, 135, 0),\n        intArrayOf(255, 137, 1),\n        intArrayOf(255, 140, 1),\n        intArrayOf(255, 142, 1),\n        intArrayOf(255, 144, 0),\n        intArrayOf(255, 148, 0),\n        intArrayOf(255, 149, 0),\n        intArrayOf(255, 152, 0),\n        intArrayOf(255, 154, 0),\n        intArrayOf(255, 158, 1),\n        intArrayOf(255, 159, 0),\n        intArrayOf(255, 161, 1),\n        intArrayOf(255, 164, 1),\n        intArrayOf(255, 166, 0),\n        intArrayOf(255, 169, 2),\n        intArrayOf(254, 172, 0),\n        intArrayOf(255, 175, 0),\n        intArrayOf(253, 178, 0),\n        intArrayOf(255, 180, 0),\n        intArrayOf(253, 183, 0),\n        intArrayOf(255, 185, 0),\n        intArrayOf(255, 187, 0),\n        intArrayOf(255, 190, 0),\n        intArrayOf(255, 192, 0),\n        intArrayOf(255, 194, 1),\n        intArrayOf(255, 197, 1),\n        intArrayOf(255, 199, 1),\n        intArrayOf(255, 202, 0),\n        intArrayOf(255, 204, 0),\n        intArrayOf(255, 207, 0),\n        intArrayOf(255, 210, 0),\n        intArrayOf(255, 212, 0),\n        intArrayOf(255, 214, 0),\n        intArrayOf(254, 218, 0),\n        intArrayOf(255, 220, 0),\n        intArrayOf(255, 223, 0),\n        intArrayOf(254, 225, 1),\n        intArrayOf(254, 227, 0),\n        intArrayOf(254, 230, 0),\n        intArrayOf(254, 232, 0),\n        intArrayOf(255, 234, 1),\n        intArrayOf(254, 237, 0),\n        intArrayOf(255, 239, 0),\n        intArrayOf(254, 242, 0),\n        intArrayOf(255, 245, 0),\n        intArrayOf(253, 248, 0),\n        intArrayOf(255, 250, 1),\n        intArrayOf(255, 251, 2),\n        intArrayOf(254, 253, 5),\n        intArrayOf(255, 254, 6),\n        intArrayOf(255, 255, 11),\n        intArrayOf(255, 255, 14),\n        intArrayOf(255, 255, 21),\n        intArrayOf(255, 255, 25),\n        intArrayOf(255, 255, 30),\n        intArrayOf(255, 255, 33),\n        intArrayOf(255, 254, 40),\n        intArrayOf(255, 254, 45),\n        intArrayOf(255, 255, 51),\n        intArrayOf(254, 255, 55),\n        intArrayOf(255, 255, 59),\n        intArrayOf(254, 255, 63),\n        intArrayOf(254, 255, 69),\n        intArrayOf(254, 255, 73),\n        intArrayOf(255, 255, 81),\n        intArrayOf(255, 255, 85),\n        intArrayOf(254, 255, 89),\n        intArrayOf(255, 255, 93),\n        intArrayOf(254, 255, 101),\n        intArrayOf(255, 255, 105),\n        intArrayOf(255, 255, 109),\n        intArrayOf(255, 255, 113),\n        intArrayOf(255, 255, 121),\n        intArrayOf(255, 255, 125),\n        intArrayOf(255, 255, 131),\n        intArrayOf(255, 255, 135),\n        intArrayOf(255, 255, 139),\n        intArrayOf(255, 255, 143),\n        intArrayOf(254, 255, 149),\n        intArrayOf(255, 255, 154),\n        intArrayOf(255, 255, 162),\n        intArrayOf(255, 255, 165),\n        intArrayOf(255, 255, 169),\n        intArrayOf(255, 255, 173),\n        intArrayOf(254, 254, 180),\n        intArrayOf(255, 255, 185),\n        intArrayOf(255, 255, 190),\n        intArrayOf(255, 255, 193),\n        intArrayOf(255, 255, 201),\n        intArrayOf(255, 254, 205),\n        intArrayOf(255, 255, 211),\n        intArrayOf(254, 255, 215),\n        intArrayOf(255, 255, 219),\n        intArrayOf(255, 255, 224),\n        intArrayOf(253, 255, 229),\n        intArrayOf(254, 255, 234),\n        intArrayOf(254, 255, 241),\n        intArrayOf(255, 255, 245),\n        intArrayOf(254, 255, 249),\n        intArrayOf(255, 255, 251)\n    )\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/lut/HsvLUT.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.lut\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.lut.HsvLUT\n * @author: Tony Shen\n * @date: 2024/6/17 14:17\n * @version: V1.0 <描述当前版本功能>\n */\n\nobject HsvLUT {\n    var HSV_LUT = arrayOf(\n        intArrayOf(253, 1, 0),\n        intArrayOf(255, 6, 0),\n        intArrayOf(255, 11, 1),\n        intArrayOf(254, 17, 1),\n        intArrayOf(255, 24, 1),\n        intArrayOf(255, 30, 0),\n        intArrayOf(254, 36, 0),\n        intArrayOf(255, 42, 0),\n        intArrayOf(254, 48, 0),\n        intArrayOf(255, 54, 0),\n        intArrayOf(254, 60, 0),\n        intArrayOf(254, 67, 0),\n        intArrayOf(255, 73, 0),\n        intArrayOf(254, 79, 0),\n        intArrayOf(254, 84, 0),\n        intArrayOf(255, 90, 0),\n        intArrayOf(255, 97, 0),\n        intArrayOf(255, 102, 0),\n        intArrayOf(255, 108, 2),\n        intArrayOf(255, 113, 1),\n        intArrayOf(255, 120, 0),\n        intArrayOf(255, 126, 0),\n        intArrayOf(255, 133, 0),\n        intArrayOf(255, 138, 0),\n        intArrayOf(255, 144, 0),\n        intArrayOf(255, 150, 0),\n        intArrayOf(255, 156, 0),\n        intArrayOf(254, 162, 0),\n        intArrayOf(255, 169, 0),\n        intArrayOf(254, 175, 0),\n        intArrayOf(255, 180, 0),\n        intArrayOf(255, 185, 0),\n        intArrayOf(255, 192, 0),\n        intArrayOf(255, 197, 0),\n        intArrayOf(255, 203, 1),\n        intArrayOf(255, 210, 2),\n        intArrayOf(255, 216, 0),\n        intArrayOf(255, 222, 0),\n        intArrayOf(255, 229, 0),\n        intArrayOf(255, 234, 0),\n        intArrayOf(255, 240, 1),\n        intArrayOf(253, 245, 0),\n        intArrayOf(252, 248, 1),\n        intArrayOf(246, 251, 0),\n        intArrayOf(243, 252, 1),\n        intArrayOf(239, 254, 1),\n        intArrayOf(235, 255, 0),\n        intArrayOf(229, 255, 0),\n        intArrayOf(222, 255, 0),\n        intArrayOf(215, 255, 0),\n        intArrayOf(209, 255, 0),\n        intArrayOf(204, 255, 0),\n        intArrayOf(198, 255, 0),\n        intArrayOf(190, 255, 0),\n        intArrayOf(185, 255, 1),\n        intArrayOf(180, 255, 0),\n        intArrayOf(174, 255, 0),\n        intArrayOf(168, 255, 0),\n        intArrayOf(163, 254, 0),\n        intArrayOf(155, 255, 0),\n        intArrayOf(150, 255, 0),\n        intArrayOf(144, 255, 0),\n        intArrayOf(138, 255, 1),\n        intArrayOf(132, 255, 0),\n        intArrayOf(125, 255, 0),\n        intArrayOf(120, 255, 0),\n        intArrayOf(114, 255, 0),\n        intArrayOf(108, 255, 0),\n        intArrayOf(101, 255, 0),\n        intArrayOf(95, 255, 0),\n        intArrayOf(90, 255, 2),\n        intArrayOf(84, 255, 0),\n        intArrayOf(78, 255, 0),\n        intArrayOf(71, 255, 0),\n        intArrayOf(67, 254, 0),\n        intArrayOf(60, 255, 0),\n        intArrayOf(54, 255, 0),\n        intArrayOf(48, 255, 0),\n        intArrayOf(41, 255, 1),\n        intArrayOf(36, 255, 0),\n        intArrayOf(30, 255, 0),\n        intArrayOf(24, 255, 0),\n        intArrayOf(18, 255, 0),\n        intArrayOf(11, 255, 0),\n        intArrayOf(7, 254, 0),\n        intArrayOf(1, 254, 3),\n        intArrayOf(0, 254, 6),\n        intArrayOf(0, 255, 11),\n        intArrayOf(0, 255, 19),\n        intArrayOf(0, 255, 24),\n        intArrayOf(0, 255, 31),\n        intArrayOf(1, 255, 37),\n        intArrayOf(1, 255, 45),\n        intArrayOf(1, 254, 49),\n        intArrayOf(0, 255, 55),\n        intArrayOf(0, 255, 61),\n        intArrayOf(0, 255, 67),\n        intArrayOf(0, 255, 73),\n        intArrayOf(0, 255, 79),\n        intArrayOf(0, 255, 83),\n        intArrayOf(0, 255, 91),\n        intArrayOf(1, 255, 97),\n        intArrayOf(1, 255, 104),\n        intArrayOf(0, 255, 109),\n        intArrayOf(0, 255, 115),\n        intArrayOf(0, 255, 120),\n        intArrayOf(0, 255, 127),\n        intArrayOf(0, 255, 133),\n        intArrayOf(0, 254, 140),\n        intArrayOf(1, 254, 145),\n        intArrayOf(0, 255, 151),\n        intArrayOf(0, 254, 156),\n        intArrayOf(0, 255, 163),\n        intArrayOf(1, 255, 169),\n        intArrayOf(0, 255, 175),\n        intArrayOf(0, 254, 181),\n        intArrayOf(0, 255, 187),\n        intArrayOf(1, 255, 193),\n        intArrayOf(1, 255, 201),\n        intArrayOf(1, 255, 205),\n        intArrayOf(0, 255, 213),\n        intArrayOf(1, 255, 218),\n        intArrayOf(0, 255, 225),\n        intArrayOf(1, 255, 229),\n        intArrayOf(1, 254, 236),\n        intArrayOf(2, 254, 241),\n        intArrayOf(0, 253, 243),\n        intArrayOf(0, 251, 246),\n        intArrayOf(0, 247, 249),\n        intArrayOf(1, 245, 253),\n        intArrayOf(0, 240, 253),\n        intArrayOf(0, 234, 253),\n        intArrayOf(0, 228, 255),\n        intArrayOf(0, 222, 255),\n        intArrayOf(0, 216, 255),\n        intArrayOf(0, 210, 255),\n        intArrayOf(0, 205, 255),\n        intArrayOf(0, 199, 255),\n        intArrayOf(0, 193, 255),\n        intArrayOf(0, 187, 255),\n        intArrayOf(0, 179, 254),\n        intArrayOf(1, 173, 255),\n        intArrayOf(0, 168, 255),\n        intArrayOf(0, 162, 255),\n        intArrayOf(0, 156, 255),\n        intArrayOf(0, 150, 255),\n        intArrayOf(0, 145, 254),\n        intArrayOf(0, 140, 255),\n        intArrayOf(0, 131, 255),\n        intArrayOf(0, 126, 255),\n        intArrayOf(0, 120, 255),\n        intArrayOf(0, 114, 255),\n        intArrayOf(0, 107, 255),\n        intArrayOf(0, 102, 255),\n        intArrayOf(0, 96, 255),\n        intArrayOf(0, 90, 255),\n        intArrayOf(1, 83, 255),\n        intArrayOf(1, 77, 255),\n        intArrayOf(1, 71, 255),\n        intArrayOf(1, 66, 255),\n        intArrayOf(0, 59, 255),\n        intArrayOf(0, 55, 255),\n        intArrayOf(0, 48, 255),\n        intArrayOf(0, 43, 254),\n        intArrayOf(1, 35, 254),\n        intArrayOf(1, 30, 254),\n        intArrayOf(1, 23, 255),\n        intArrayOf(1, 18, 255),\n        intArrayOf(0, 12, 255),\n        intArrayOf(0, 7, 255),\n        intArrayOf(0, 1, 252),\n        intArrayOf(6, 0, 254),\n        intArrayOf(12, 1, 255),\n        intArrayOf(17, 0, 254),\n        intArrayOf(26, 0, 255),\n        intArrayOf(30, 0, 254),\n        intArrayOf(37, 0, 254),\n        intArrayOf(42, 1, 255),\n        intArrayOf(47, 0, 254),\n        intArrayOf(52, 1, 254),\n        intArrayOf(60, 0, 254),\n        intArrayOf(66, 1, 253),\n        intArrayOf(72, 1, 255),\n        intArrayOf(77, 0, 254),\n        intArrayOf(83, 0, 254),\n        intArrayOf(90, 0, 254),\n        intArrayOf(96, 0, 255),\n        intArrayOf(102, 1, 255),\n        intArrayOf(109, 0, 254),\n        intArrayOf(113, 0, 254),\n        intArrayOf(120, 0, 255),\n        intArrayOf(126, 1, 255),\n        intArrayOf(132, 1, 255),\n        intArrayOf(137, 0, 254),\n        intArrayOf(144, 0, 255),\n        intArrayOf(148, 1, 255),\n        intArrayOf(156, 1, 255),\n        intArrayOf(161, 0, 252),\n        intArrayOf(167, 0, 254),\n        intArrayOf(174, 0, 255),\n        intArrayOf(180, 0, 255),\n        intArrayOf(186, 1, 255),\n        intArrayOf(191, 0, 255),\n        intArrayOf(197, 0, 255),\n        intArrayOf(205, 0, 255),\n        intArrayOf(210, 0, 255),\n        intArrayOf(216, 1, 255),\n        intArrayOf(220, 0, 254),\n        intArrayOf(227, 0, 255),\n        intArrayOf(232, 1, 253),\n        intArrayOf(238, 1, 255),\n        intArrayOf(242, 1, 253),\n        intArrayOf(246, 0, 249),\n        intArrayOf(250, 0, 247),\n        intArrayOf(253, 0, 243),\n        intArrayOf(255, 0, 240),\n        intArrayOf(255, 0, 234),\n        intArrayOf(255, 0, 228),\n        intArrayOf(255, 1, 222),\n        intArrayOf(254, 1, 214),\n        intArrayOf(254, 0, 210),\n        intArrayOf(253, 0, 204),\n        intArrayOf(255, 0, 200),\n        intArrayOf(255, 0, 194),\n        intArrayOf(254, 0, 186),\n        intArrayOf(254, 1, 180),\n        intArrayOf(254, 0, 174),\n        intArrayOf(253, 0, 168),\n        intArrayOf(255, 0, 162),\n        intArrayOf(255, 0, 154),\n        intArrayOf(255, 0, 150),\n        intArrayOf(255, 0, 144),\n        intArrayOf(254, 0, 137),\n        intArrayOf(254, 0, 132),\n        intArrayOf(255, 0, 128),\n        intArrayOf(254, 1, 120),\n        intArrayOf(255, 0, 114),\n        intArrayOf(255, 0, 108),\n        intArrayOf(255, 0, 102),\n        intArrayOf(255, 0, 96),\n        intArrayOf(255, 0, 91),\n        intArrayOf(255, 0, 84),\n        intArrayOf(255, 0, 78),\n        intArrayOf(254, 0, 72),\n        intArrayOf(255, 0, 66),\n        intArrayOf(254, 1, 58),\n        intArrayOf(255, 0, 54),\n        intArrayOf(255, 0, 48),\n        intArrayOf(255, 0, 41),\n        intArrayOf(255, 0, 36),\n        intArrayOf(255, 0, 32),\n        intArrayOf(255, 0, 24),\n        intArrayOf(255, 0, 18),\n        intArrayOf(255, 0, 12),\n        intArrayOf(255, 0, 6),\n        intArrayOf(254, 0, 3)\n    )\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/lut/JetLUT.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.lut\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.lut.JetLUT\n * @author: Tony Shen\n * @date: 2024/6/17 14:18\n * @version: V1.0 <描述当前版本功能>\n */\n\nobject JetLUT {\n    var JET_LUT = arrayOf(\n        intArrayOf(0, 1, 128),\n        intArrayOf(0, 0, 130),\n        intArrayOf(0, 0, 132),\n        intArrayOf(1, 0, 136),\n        intArrayOf(0, 0, 142),\n        intArrayOf(0, 0, 145),\n        intArrayOf(0, 0, 150),\n        intArrayOf(1, 0, 154),\n        intArrayOf(0, 0, 158),\n        intArrayOf(1, 0, 163),\n        intArrayOf(0, 0, 166),\n        intArrayOf(0, 0, 170),\n        intArrayOf(0, 0, 175),\n        intArrayOf(0, 0, 179),\n        intArrayOf(0, 0, 182),\n        intArrayOf(0, 0, 186),\n        intArrayOf(0, 1, 190),\n        intArrayOf(0, 0, 194),\n        intArrayOf(1, 1, 197),\n        intArrayOf(1, 0, 200),\n        intArrayOf(0, 1, 207),\n        intArrayOf(0, 0, 210),\n        intArrayOf(1, 0, 215),\n        intArrayOf(1, 0, 218),\n        intArrayOf(0, 0, 222),\n        intArrayOf(1, 0, 226),\n        intArrayOf(0, 0, 230),\n        intArrayOf(0, 0, 234),\n        intArrayOf(0, 0, 240),\n        intArrayOf(0, 0, 244),\n        intArrayOf(0, 0, 246),\n        intArrayOf(0, 1, 249),\n        intArrayOf(1, 2, 253),\n        intArrayOf(0, 5, 254),\n        intArrayOf(0, 8, 255),\n        intArrayOf(1, 13, 255),\n        intArrayOf(0, 17, 255),\n        intArrayOf(0, 21, 254),\n        intArrayOf(1, 25, 255),\n        intArrayOf(0, 29, 255),\n        intArrayOf(0, 33, 255),\n        intArrayOf(0, 38, 255),\n        intArrayOf(0, 41, 255),\n        intArrayOf(0, 44, 254),\n        intArrayOf(1, 49, 255),\n        intArrayOf(0, 53, 253),\n        intArrayOf(0, 56, 255),\n        intArrayOf(1, 61, 255),\n        intArrayOf(0, 65, 255),\n        intArrayOf(0, 70, 254),\n        intArrayOf(0, 73, 255),\n        intArrayOf(0, 77, 255),\n        intArrayOf(0, 82, 255),\n        intArrayOf(0, 86, 254),\n        intArrayOf(0, 89, 255),\n        intArrayOf(0, 94, 254),\n        intArrayOf(0, 97, 255),\n        intArrayOf(0, 101, 255),\n        intArrayOf(1, 105, 255),\n        intArrayOf(0, 109, 254),\n        intArrayOf(0, 113, 254),\n        intArrayOf(0, 118, 254),\n        intArrayOf(0, 120, 255),\n        intArrayOf(0, 124, 253),\n        intArrayOf(0, 128, 255),\n        intArrayOf(0, 133, 254),\n        intArrayOf(1, 135, 255),\n        intArrayOf(0, 140, 255),\n        intArrayOf(0, 144, 255),\n        intArrayOf(0, 148, 254),\n        intArrayOf(0, 151, 254),\n        intArrayOf(0, 157, 254),\n        intArrayOf(0, 160, 255),\n        intArrayOf(0, 164, 254),\n        intArrayOf(0, 168, 255),\n        intArrayOf(0, 172, 254),\n        intArrayOf(0, 175, 254),\n        intArrayOf(0, 180, 254),\n        intArrayOf(1, 183, 255),\n        intArrayOf(0, 187, 254),\n        intArrayOf(0, 192, 255),\n        intArrayOf(0, 196, 254),\n        intArrayOf(0, 199, 255),\n        intArrayOf(0, 204, 255),\n        intArrayOf(0, 208, 255),\n        intArrayOf(0, 213, 255),\n        intArrayOf(0, 216, 255),\n        intArrayOf(0, 220, 254),\n        intArrayOf(1, 226, 255),\n        intArrayOf(0, 229, 255),\n        intArrayOf(0, 232, 255),\n        intArrayOf(0, 237, 255),\n        intArrayOf(0, 240, 255),\n        intArrayOf(0, 244, 254),\n        intArrayOf(1, 248, 255),\n        intArrayOf(2, 251, 255),\n        intArrayOf(4, 254, 252),\n        intArrayOf(6, 255, 249),\n        intArrayOf(10, 255, 245),\n        intArrayOf(13, 255, 241),\n        intArrayOf(18, 255, 237),\n        intArrayOf(22, 255, 233),\n        intArrayOf(27, 255, 230),\n        intArrayOf(30, 255, 225),\n        intArrayOf(34, 255, 222),\n        intArrayOf(37, 255, 218),\n        intArrayOf(43, 255, 215),\n        intArrayOf(45, 255, 208),\n        intArrayOf(52, 254, 206),\n        intArrayOf(56, 254, 201),\n        intArrayOf(60, 255, 197),\n        intArrayOf(62, 255, 192),\n        intArrayOf(66, 255, 189),\n        intArrayOf(70, 255, 185),\n        intArrayOf(73, 255, 181),\n        intArrayOf(77, 255, 177),\n        intArrayOf(82, 255, 175),\n        intArrayOf(86, 255, 168),\n        intArrayOf(90, 254, 165),\n        intArrayOf(94, 255, 161),\n        intArrayOf(97, 255, 158),\n        intArrayOf(101, 255, 154),\n        intArrayOf(105, 254, 150),\n        intArrayOf(109, 255, 144),\n        intArrayOf(116, 254, 142),\n        intArrayOf(120, 255, 137),\n        intArrayOf(122, 254, 132),\n        intArrayOf(126, 255, 128),\n        intArrayOf(129, 255, 125),\n        intArrayOf(131, 255, 120),\n        intArrayOf(135, 255, 117),\n        intArrayOf(139, 255, 113),\n        intArrayOf(146, 255, 110),\n        intArrayOf(149, 255, 105),\n        intArrayOf(154, 255, 101),\n        intArrayOf(158, 255, 98),\n        intArrayOf(161, 255, 94),\n        intArrayOf(164, 255, 90),\n        intArrayOf(169, 255, 86),\n        intArrayOf(173, 255, 82),\n        intArrayOf(178, 255, 79),\n        intArrayOf(181, 255, 74),\n        intArrayOf(185, 255, 69),\n        intArrayOf(189, 255, 65),\n        intArrayOf(193, 254, 62),\n        intArrayOf(197, 255, 57),\n        intArrayOf(201, 255, 55),\n        intArrayOf(204, 255, 50),\n        intArrayOf(209, 254, 47),\n        intArrayOf(212, 255, 41),\n        intArrayOf(218, 255, 38),\n        intArrayOf(221, 255, 34),\n        intArrayOf(224, 255, 30),\n        intArrayOf(228, 255, 26),\n        intArrayOf(233, 255, 24),\n        intArrayOf(237, 255, 17),\n        intArrayOf(243, 254, 14),\n        intArrayOf(246, 254, 10),\n        intArrayOf(250, 254, 7),\n        intArrayOf(254, 255, 4),\n        intArrayOf(255, 251, 2),\n        intArrayOf(255, 248, 0),\n        intArrayOf(254, 244, 0),\n        intArrayOf(255, 240, 0),\n        intArrayOf(254, 237, 0),\n        intArrayOf(254, 232, 0),\n        intArrayOf(255, 228, 0),\n        intArrayOf(255, 225, 0),\n        intArrayOf(254, 220, 0),\n        intArrayOf(255, 216, 0),\n        intArrayOf(255, 212, 0),\n        intArrayOf(255, 207, 0),\n        intArrayOf(255, 204, 1),\n        intArrayOf(255, 200, 1),\n        intArrayOf(254, 196, 1),\n        intArrayOf(255, 192, 0),\n        intArrayOf(255, 188, 1),\n        intArrayOf(254, 184, 0),\n        intArrayOf(255, 180, 0),\n        intArrayOf(254, 177, 0),\n        intArrayOf(254, 172, 0),\n        intArrayOf(255, 168, 1),\n        intArrayOf(255, 164, 1),\n        intArrayOf(255, 159, 0),\n        intArrayOf(255, 156, 1),\n        intArrayOf(255, 151, 0),\n        intArrayOf(255, 148, 0),\n        intArrayOf(255, 143, 2),\n        intArrayOf(255, 139, 0),\n        intArrayOf(255, 136, 0),\n        intArrayOf(255, 132, 2),\n        intArrayOf(255, 128, 0),\n        intArrayOf(255, 125, 1),\n        intArrayOf(255, 121, 0),\n        intArrayOf(255, 117, 0),\n        intArrayOf(255, 113, 0),\n        intArrayOf(255, 108, 0),\n        intArrayOf(255, 104, 0),\n        intArrayOf(255, 101, 0),\n        intArrayOf(255, 97, 0),\n        intArrayOf(255, 93, 0),\n        intArrayOf(254, 89, 0),\n        intArrayOf(254, 85, 0),\n        intArrayOf(254, 81, 2),\n        intArrayOf(255, 76, 1),\n        intArrayOf(255, 72, 0),\n        intArrayOf(255, 69, 2),\n        intArrayOf(255, 64, 0),\n        intArrayOf(255, 61, 0),\n        intArrayOf(255, 57, 0),\n        intArrayOf(254, 53, 0),\n        intArrayOf(255, 49, 0),\n        intArrayOf(254, 46, 0),\n        intArrayOf(254, 41, 1),\n        intArrayOf(255, 37, 1),\n        intArrayOf(255, 33, 0),\n        intArrayOf(253, 28, 0),\n        intArrayOf(255, 25, 1),\n        intArrayOf(255, 21, 0),\n        intArrayOf(255, 16, 1),\n        intArrayOf(255, 13, 1),\n        intArrayOf(255, 9, 0),\n        intArrayOf(254, 5, 1),\n        intArrayOf(252, 3, 0),\n        intArrayOf(250, 0, 1),\n        intArrayOf(246, 1, 0),\n        intArrayOf(244, 0, 0),\n        intArrayOf(239, 0, 0),\n        intArrayOf(234, 0, 1),\n        intArrayOf(230, 0, 0),\n        intArrayOf(226, 0, 1),\n        intArrayOf(223, 1, 0),\n        intArrayOf(218, 0, 0),\n        intArrayOf(214, 0, 0),\n        intArrayOf(210, 0, 0),\n        intArrayOf(206, 0, 0),\n        intArrayOf(201, 1, 1),\n        intArrayOf(197, 1, 2),\n        intArrayOf(194, 0, 1),\n        intArrayOf(190, 0, 0),\n        intArrayOf(186, 1, 0),\n        intArrayOf(183, 1, 0),\n        intArrayOf(180, 0, 0),\n        intArrayOf(175, 0, 0),\n        intArrayOf(170, 0, 0),\n        intArrayOf(166, 1, 0),\n        intArrayOf(163, 0, 1),\n        intArrayOf(159, 1, 0),\n        intArrayOf(154, 0, 0),\n        intArrayOf(150, 0, 0),\n        intArrayOf(146, 0, 1),\n        intArrayOf(143, 1, 0),\n        intArrayOf(139, 1, 1),\n        intArrayOf(134, 0, 0),\n        intArrayOf(132, 0, 0),\n        intArrayOf(129, 0, 0)\n    )\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/lut/LUT.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.lut\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.lut.LUT\n * @author: Tony Shen\n * @date: 2024/6/16 15:43\n * @version: V1.0 <描述当前版本功能>\n */\nconst val AUTUMN_STYLE: Int = 0\nconst val BONE_STYLE = 1\nconst val COOL_STYLE = 2\nconst val HOT_STYLE = 3\nconst val HSV_STYLE = 4\nconst val JET_STYLE = 5\nconst val OCEAN_STYLE = 6\nconst val PINK_STYLE = 7\nconst val RAINBOW_STYLE = 8\nconst val SPRING_STYLE = 9\nconst val SUMMER_STYLE = 10\nconst val WINTER_STYLE = 11\n\nfun getColorFilterLUT(style: Int): Array<IntArray> {\n\n    return when (style) {\n        AUTUMN_STYLE  -> AutumnLUT.AUTUMN_LUT\n        BONE_STYLE    -> BoneLUT.BONE_LUT\n        COOL_STYLE    -> CoolLUT.COOL_LUT\n        HOT_STYLE     -> HotLUT.HOT_LUT\n        HSV_STYLE     -> HsvLUT.HSV_LUT\n        JET_STYLE     -> JetLUT.JET_LUT\n        OCEAN_STYLE   -> OceanLUT.OCEAN_LUT\n        PINK_STYLE    -> PinkLUT.PINK_LUT\n        RAINBOW_STYLE -> RainbowLUT.RAINBOW_LUT\n        SPRING_STYLE  -> SpringLUT.SPRING_LUT\n        SUMMER_STYLE  -> SummerLUT.SUMMER_LUT\n        WINTER_STYLE  -> WinterLUT.WINTER_LUT\n        else          -> AutumnLUT.AUTUMN_LUT\n    }\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/lut/OceanLUT.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.lut\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.lut.OceanLUT\n * @author: Tony Shen\n * @date: 2024/6/17 14:33\n * @version: V1.0 <描述当前版本功能>\n */\n\nobject OceanLUT {\n    var OCEAN_LUT = arrayOf(\n        intArrayOf(0, 0, 0),\n        intArrayOf(0, 0, 0),\n        intArrayOf(0, 0, 2),\n        intArrayOf(0, 0, 2),\n        intArrayOf(0, 0, 4),\n        intArrayOf(1, 0, 5),\n        intArrayOf(1, 0, 6),\n        intArrayOf(1, 0, 6),\n        intArrayOf(0, 0, 8),\n        intArrayOf(0, 0, 8),\n        intArrayOf(0, 0, 10),\n        intArrayOf(0, 0, 10),\n        intArrayOf(0, 0, 12),\n        intArrayOf(0, 0, 12),\n        intArrayOf(1, 0, 14),\n        intArrayOf(1, 0, 14),\n        intArrayOf(1, 0, 16),\n        intArrayOf(1, 0, 16),\n        intArrayOf(1, 0, 18),\n        intArrayOf(1, 0, 18),\n        intArrayOf(0, 0, 20),\n        intArrayOf(0, 0, 20),\n        intArrayOf(0, 1, 22),\n        intArrayOf(0, 1, 22),\n        intArrayOf(0, 0, 24),\n        intArrayOf(0, 0, 24),\n        intArrayOf(0, 0, 26),\n        intArrayOf(0, 0, 26),\n        intArrayOf(0, 0, 28),\n        intArrayOf(0, 0, 28),\n        intArrayOf(0, 0, 30),\n        intArrayOf(1, 0, 31),\n        intArrayOf(0, 1, 32),\n        intArrayOf(0, 1, 32),\n        intArrayOf(0, 0, 34),\n        intArrayOf(0, 0, 34),\n        intArrayOf(0, 0, 36),\n        intArrayOf(0, 0, 36),\n        intArrayOf(0, 0, 38),\n        intArrayOf(0, 0, 38),\n        intArrayOf(1, 0, 40),\n        intArrayOf(1, 0, 40),\n        intArrayOf(1, 0, 42),\n        intArrayOf(1, 0, 42),\n        intArrayOf(0, 0, 44),\n        intArrayOf(0, 0, 44),\n        intArrayOf(0, 0, 46),\n        intArrayOf(0, 0, 46),\n        intArrayOf(0, 0, 48),\n        intArrayOf(0, 1, 49),\n        intArrayOf(0, 0, 50),\n        intArrayOf(0, 0, 50),\n        intArrayOf(0, 0, 52),\n        intArrayOf(0, 0, 52),\n        intArrayOf(0, 0, 54),\n        intArrayOf(0, 0, 54),\n        intArrayOf(0, 0, 56),\n        intArrayOf(0, 0, 56),\n        intArrayOf(0, 1, 58),\n        intArrayOf(0, 1, 58),\n        intArrayOf(0, 0, 60),\n        intArrayOf(0, 0, 60),\n        intArrayOf(0, 0, 62),\n        intArrayOf(0, 0, 62),\n        intArrayOf(0, 0, 64),\n        intArrayOf(0, 0, 64),\n        intArrayOf(1, 0, 66),\n        intArrayOf(1, 0, 66),\n        intArrayOf(0, 1, 68),\n        intArrayOf(0, 1, 68),\n        intArrayOf(0, 0, 70),\n        intArrayOf(0, 0, 70),\n        intArrayOf(0, 0, 72),\n        intArrayOf(0, 0, 72),\n        intArrayOf(0, 0, 74),\n        intArrayOf(1, 1, 75),\n        intArrayOf(1, 0, 76),\n        intArrayOf(1, 0, 76),\n        intArrayOf(1, 0, 78),\n        intArrayOf(1, 0, 78),\n        intArrayOf(0, 0, 80),\n        intArrayOf(0, 0, 80),\n        intArrayOf(0, 0, 82),\n        intArrayOf(0, 0, 82),\n        intArrayOf(0, 0, 86),\n        intArrayOf(0, 0, 86),\n        intArrayOf(0, 2, 87),\n        intArrayOf(1, 3, 88),\n        intArrayOf(0, 5, 87),\n        intArrayOf(0, 7, 88),\n        intArrayOf(1, 8, 89),\n        intArrayOf(0, 10, 90),\n        intArrayOf(0, 11, 91),\n        intArrayOf(0, 13, 92),\n        intArrayOf(0, 14, 95),\n        intArrayOf(0, 15, 96),\n        intArrayOf(1, 16, 97),\n        intArrayOf(1, 18, 98),\n        intArrayOf(2, 19, 99),\n        intArrayOf(0, 21, 100),\n        intArrayOf(1, 22, 101),\n        intArrayOf(0, 25, 102),\n        intArrayOf(0, 26, 103),\n        intArrayOf(0, 27, 104),\n        intArrayOf(1, 28, 105),\n        intArrayOf(1, 30, 106),\n        intArrayOf(2, 31, 107),\n        intArrayOf(0, 34, 108),\n        intArrayOf(1, 35, 109),\n        intArrayOf(0, 37, 110),\n        intArrayOf(0, 38, 111),\n        intArrayOf(0, 40, 112),\n        intArrayOf(0, 41, 113),\n        intArrayOf(0, 42, 114),\n        intArrayOf(0, 44, 115),\n        intArrayOf(0, 46, 116),\n        intArrayOf(0, 47, 117),\n        intArrayOf(0, 49, 118),\n        intArrayOf(0, 50, 119),\n        intArrayOf(1, 51, 120),\n        intArrayOf(0, 53, 121),\n        intArrayOf(0, 53, 121),\n        intArrayOf(1, 56, 123),\n        intArrayOf(0, 56, 123),\n        intArrayOf(1, 58, 125),\n        intArrayOf(0, 59, 125),\n        intArrayOf(1, 62, 127),\n        intArrayOf(1, 62, 127),\n        intArrayOf(1, 65, 127),\n        intArrayOf(0, 66, 127),\n        intArrayOf(2, 68, 129),\n        intArrayOf(0, 69, 129),\n        intArrayOf(1, 71, 133),\n        intArrayOf(0, 72, 133),\n        intArrayOf(0, 74, 135),\n        intArrayOf(0, 75, 135),\n        intArrayOf(1, 77, 137),\n        intArrayOf(0, 78, 137),\n        intArrayOf(1, 80, 139),\n        intArrayOf(0, 81, 139),\n        intArrayOf(1, 83, 141),\n        intArrayOf(0, 84, 141),\n        intArrayOf(0, 86, 143),\n        intArrayOf(0, 87, 143),\n        intArrayOf(2, 88, 145),\n        intArrayOf(0, 89, 145),\n        intArrayOf(2, 91, 147),\n        intArrayOf(0, 93, 147),\n        intArrayOf(1, 95, 149),\n        intArrayOf(0, 96, 149),\n        intArrayOf(1, 98, 151),\n        intArrayOf(0, 99, 151),\n        intArrayOf(1, 101, 153),\n        intArrayOf(0, 101, 153),\n        intArrayOf(2, 103, 155),\n        intArrayOf(0, 105, 155),\n        intArrayOf(1, 107, 157),\n        intArrayOf(0, 108, 157),\n        intArrayOf(0, 110, 159),\n        intArrayOf(0, 111, 159),\n        intArrayOf(0, 114, 161),\n        intArrayOf(0, 114, 161),\n        intArrayOf(0, 116, 163),\n        intArrayOf(0, 117, 163),\n        intArrayOf(1, 119, 165),\n        intArrayOf(0, 120, 165),\n        intArrayOf(0, 123, 167),\n        intArrayOf(0, 123, 167),\n        intArrayOf(0, 125, 169),\n        intArrayOf(0, 125, 169),\n        intArrayOf(4, 127, 169),\n        intArrayOf(5, 128, 170),\n        intArrayOf(8, 130, 171),\n        intArrayOf(10, 132, 173),\n        intArrayOf(11, 133, 174),\n        intArrayOf(14, 136, 175),\n        intArrayOf(18, 136, 176),\n        intArrayOf(20, 138, 176),\n        intArrayOf(25, 138, 178),\n        intArrayOf(27, 140, 180),\n        intArrayOf(32, 141, 180),\n        intArrayOf(34, 143, 182),\n        intArrayOf(37, 145, 183),\n        intArrayOf(39, 147, 183),\n        intArrayOf(41, 148, 184),\n        intArrayOf(44, 151, 185),\n        intArrayOf(47, 152, 184),\n        intArrayOf(48, 153, 185),\n        intArrayOf(54, 154, 186),\n        intArrayOf(56, 156, 188),\n        intArrayOf(60, 157, 190),\n        intArrayOf(62, 159, 191),\n        intArrayOf(66, 161, 193),\n        intArrayOf(67, 162, 192),\n        intArrayOf(73, 164, 195),\n        intArrayOf(75, 166, 197),\n        intArrayOf(79, 166, 196),\n        intArrayOf(81, 168, 198),\n        intArrayOf(85, 168, 198),\n        intArrayOf(87, 171, 199),\n        intArrayOf(91, 172, 201),\n        intArrayOf(92, 173, 200),\n        intArrayOf(96, 176, 201),\n        intArrayOf(98, 178, 203),\n        intArrayOf(102, 178, 202),\n        intArrayOf(104, 180, 204),\n        intArrayOf(109, 181, 206),\n        intArrayOf(111, 183, 207),\n        intArrayOf(114, 184, 209),\n        intArrayOf(116, 187, 209),\n        intArrayOf(120, 188, 211),\n        intArrayOf(122, 190, 213),\n        intArrayOf(125, 190, 212),\n        intArrayOf(128, 193, 215),\n        intArrayOf(133, 194, 215),\n        intArrayOf(134, 195, 214),\n        intArrayOf(138, 196, 216),\n        intArrayOf(140, 199, 217),\n        intArrayOf(144, 200, 217),\n        intArrayOf(146, 202, 219),\n        intArrayOf(151, 202, 219),\n        intArrayOf(153, 204, 221),\n        intArrayOf(157, 204, 222),\n        intArrayOf(159, 206, 222),\n        intArrayOf(162, 208, 224),\n        intArrayOf(164, 210, 225),\n        intArrayOf(169, 211, 227),\n        intArrayOf(171, 213, 229),\n        intArrayOf(177, 214, 230),\n        intArrayOf(178, 215, 231),\n        intArrayOf(180, 216, 230),\n        intArrayOf(183, 219, 231),\n        intArrayOf(186, 220, 232),\n        intArrayOf(188, 222, 232),\n        intArrayOf(191, 224, 233),\n        intArrayOf(193, 226, 235),\n        intArrayOf(198, 227, 235),\n        intArrayOf(200, 229, 237),\n        intArrayOf(205, 229, 239),\n        intArrayOf(207, 232, 239),\n        intArrayOf(209, 232, 240),\n        intArrayOf(212, 235, 241),\n        intArrayOf(217, 236, 243),\n        intArrayOf(217, 236, 243),\n        intArrayOf(223, 238, 245),\n        intArrayOf(225, 240, 247),\n        intArrayOf(230, 241, 247),\n        intArrayOf(232, 243, 247),\n        intArrayOf(234, 243, 248),\n        intArrayOf(237, 247, 249),\n        intArrayOf(240, 248, 250),\n        intArrayOf(240, 248, 250),\n        intArrayOf(246, 250, 251),\n        intArrayOf(248, 252, 253),\n        intArrayOf(253, 253, 255),\n        intArrayOf(255, 255, 255)\n    )\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/lut/PinkLUT.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.lut\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.lut.PinkLUT\n * @author: Tony Shen\n * @date: 2024/6/17 14:35\n * @version: V1.0 <描述当前版本功能>\n */\n\nobject PinkLUT {\n    var PINK_LUT = arrayOf(\n        intArrayOf(1, 0, 0),\n        intArrayOf(10, 6, 5),\n        intArrayOf(19, 14, 11),\n        intArrayOf(29, 19, 18),\n        intArrayOf(38, 26, 26),\n        intArrayOf(44, 30, 30),\n        intArrayOf(49, 31, 31),\n        intArrayOf(52, 34, 34),\n        intArrayOf(57, 37, 36),\n        intArrayOf(59, 39, 38),\n        intArrayOf(63, 42, 41),\n        intArrayOf(65, 44, 43),\n        intArrayOf(69, 45, 45),\n        intArrayOf(71, 47, 47),\n        intArrayOf(74, 48, 49),\n        intArrayOf(76, 50, 51),\n        intArrayOf(80, 52, 51),\n        intArrayOf(82, 54, 53),\n        intArrayOf(85, 55, 55),\n        intArrayOf(86, 56, 56),\n        intArrayOf(88, 58, 58),\n        intArrayOf(90, 60, 60),\n        intArrayOf(93, 61, 62),\n        intArrayOf(94, 62, 63),\n        intArrayOf(98, 64, 63),\n        intArrayOf(99, 65, 64),\n        intArrayOf(101, 65, 65),\n        intArrayOf(103, 67, 67),\n        intArrayOf(105, 69, 69),\n        intArrayOf(106, 70, 70),\n        intArrayOf(109, 70, 71),\n        intArrayOf(111, 72, 73),\n        intArrayOf(113, 75, 74),\n        intArrayOf(114, 76, 75),\n        intArrayOf(115, 77, 76),\n        intArrayOf(116, 78, 77),\n        intArrayOf(118, 78, 78),\n        intArrayOf(120, 80, 80),\n        intArrayOf(122, 80, 81),\n        intArrayOf(123, 81, 82),\n        intArrayOf(126, 82, 81),\n        intArrayOf(127, 83, 82),\n        intArrayOf(129, 83, 83),\n        intArrayOf(131, 85, 85),\n        intArrayOf(134, 86, 86),\n        intArrayOf(135, 87, 87),\n        intArrayOf(136, 88, 88),\n        intArrayOf(137, 89, 89),\n        intArrayOf(138, 90, 90),\n        intArrayOf(139, 91, 91),\n        intArrayOf(141, 93, 93),\n        intArrayOf(142, 94, 94),\n        intArrayOf(143, 95, 95),\n        intArrayOf(144, 96, 96),\n        intArrayOf(146, 96, 97),\n        intArrayOf(147, 97, 98),\n        intArrayOf(149, 98, 97),\n        intArrayOf(150, 99, 98),\n        intArrayOf(151, 100, 99),\n        intArrayOf(152, 101, 100),\n        intArrayOf(153, 102, 101),\n        intArrayOf(154, 103, 102),\n        intArrayOf(157, 103, 103),\n        intArrayOf(157, 103, 103),\n        intArrayOf(158, 104, 104),\n        intArrayOf(160, 106, 106),\n        intArrayOf(162, 106, 107),\n        intArrayOf(163, 107, 108),\n        intArrayOf(164, 108, 107),\n        intArrayOf(164, 108, 107),\n        intArrayOf(165, 109, 108),\n        intArrayOf(166, 110, 109),\n        intArrayOf(169, 111, 110),\n        intArrayOf(170, 112, 111),\n        intArrayOf(172, 112, 112),\n        intArrayOf(173, 113, 113),\n        intArrayOf(174, 114, 114),\n        intArrayOf(174, 114, 114),\n        intArrayOf(175, 115, 115),\n        intArrayOf(176, 116, 116),\n        intArrayOf(177, 117, 117),\n        intArrayOf(178, 118, 118),\n        intArrayOf(180, 118, 119),\n        intArrayOf(181, 119, 120),\n        intArrayOf(181, 120, 119),\n        intArrayOf(182, 121, 120),\n        intArrayOf(185, 121, 121),\n        intArrayOf(186, 122, 122),\n        intArrayOf(187, 121, 122),\n        intArrayOf(188, 122, 123),\n        intArrayOf(189, 123, 124),\n        intArrayOf(190, 124, 125),\n        intArrayOf(191, 126, 124),\n        intArrayOf(192, 127, 125),\n        intArrayOf(193, 128, 126),\n        intArrayOf(194, 129, 127),\n        intArrayOf(195, 130, 128),\n        intArrayOf(195, 130, 128),\n        intArrayOf(196, 131, 129),\n        intArrayOf(197, 132, 130),\n        intArrayOf(197, 133, 131),\n        intArrayOf(199, 135, 133),\n        intArrayOf(198, 137, 132),\n        intArrayOf(198, 137, 132),\n        intArrayOf(199, 140, 132),\n        intArrayOf(200, 141, 133),\n        intArrayOf(200, 143, 134),\n        intArrayOf(201, 144, 135),\n        intArrayOf(199, 145, 135),\n        intArrayOf(201, 147, 137),\n        intArrayOf(201, 149, 136),\n        intArrayOf(201, 149, 136),\n        intArrayOf(201, 152, 138),\n        intArrayOf(201, 152, 138),\n        intArrayOf(202, 153, 139),\n        intArrayOf(204, 155, 141),\n        intArrayOf(203, 156, 140),\n        intArrayOf(204, 157, 141),\n        intArrayOf(205, 159, 143),\n        intArrayOf(205, 159, 143),\n        intArrayOf(204, 161, 142),\n        intArrayOf(205, 162, 143),\n        intArrayOf(206, 163, 144),\n        intArrayOf(207, 164, 145),\n        intArrayOf(207, 166, 144),\n        intArrayOf(208, 167, 145),\n        intArrayOf(208, 167, 145),\n        intArrayOf(207, 169, 146),\n        intArrayOf(208, 170, 147),\n        intArrayOf(208, 172, 148),\n        intArrayOf(209, 173, 149),\n        intArrayOf(210, 174, 150),\n        intArrayOf(210, 176, 149),\n        intArrayOf(210, 176, 149),\n        intArrayOf(211, 177, 150),\n        intArrayOf(212, 178, 151),\n        intArrayOf(211, 180, 152),\n        intArrayOf(212, 181, 153),\n        intArrayOf(213, 182, 153),\n        intArrayOf(214, 183, 154),\n        intArrayOf(213, 184, 154),\n        intArrayOf(214, 185, 155),\n        intArrayOf(215, 186, 156),\n        intArrayOf(216, 187, 157),\n        intArrayOf(214, 188, 155),\n        intArrayOf(215, 189, 156),\n        intArrayOf(216, 190, 157),\n        intArrayOf(217, 191, 158),\n        intArrayOf(216, 192, 158),\n        intArrayOf(217, 193, 159),\n        intArrayOf(217, 194, 160),\n        intArrayOf(218, 195, 161),\n        intArrayOf(217, 197, 160),\n        intArrayOf(218, 198, 161),\n        intArrayOf(219, 199, 162),\n        intArrayOf(219, 199, 162),\n        intArrayOf(219, 201, 163),\n        intArrayOf(219, 201, 163),\n        intArrayOf(220, 202, 164),\n        intArrayOf(221, 203, 165),\n        intArrayOf(220, 205, 164),\n        intArrayOf(220, 205, 164),\n        intArrayOf(221, 206, 165),\n        intArrayOf(222, 207, 166),\n        intArrayOf(222, 209, 167),\n        intArrayOf(222, 209, 167),\n        intArrayOf(223, 210, 166),\n        intArrayOf(224, 211, 167),\n        intArrayOf(224, 213, 168),\n        intArrayOf(225, 214, 169),\n        intArrayOf(225, 214, 169),\n        intArrayOf(226, 215, 170),\n        intArrayOf(225, 217, 171),\n        intArrayOf(225, 217, 171),\n        intArrayOf(226, 218, 172),\n        intArrayOf(226, 218, 172),\n        intArrayOf(227, 219, 172),\n        intArrayOf(228, 220, 173),\n        intArrayOf(228, 222, 174),\n        intArrayOf(228, 222, 174),\n        intArrayOf(227, 223, 175),\n        intArrayOf(228, 224, 176),\n        intArrayOf(229, 225, 177),\n        intArrayOf(229, 225, 177),\n        intArrayOf(230, 227, 176),\n        intArrayOf(230, 227, 176),\n        intArrayOf(231, 228, 177),\n        intArrayOf(232, 229, 178),\n        intArrayOf(233, 230, 179),\n        intArrayOf(233, 230, 179),\n        intArrayOf(233, 231, 180),\n        intArrayOf(233, 231, 180),\n        intArrayOf(233, 233, 181),\n        intArrayOf(233, 233, 181),\n        intArrayOf(234, 234, 184),\n        intArrayOf(235, 235, 185),\n        intArrayOf(236, 235, 187),\n        intArrayOf(236, 235, 187),\n        intArrayOf(236, 235, 189),\n        intArrayOf(237, 236, 190),\n        intArrayOf(235, 236, 192),\n        intArrayOf(235, 236, 192),\n        intArrayOf(236, 237, 195),\n        intArrayOf(236, 237, 195),\n        intArrayOf(237, 238, 198),\n        intArrayOf(238, 239, 199),\n        intArrayOf(238, 238, 200),\n        intArrayOf(238, 238, 200),\n        intArrayOf(239, 239, 203),\n        intArrayOf(239, 239, 203),\n        intArrayOf(240, 240, 206),\n        intArrayOf(240, 240, 206),\n        intArrayOf(240, 239, 208),\n        intArrayOf(241, 240, 209),\n        intArrayOf(241, 240, 210),\n        intArrayOf(242, 241, 211),\n        intArrayOf(242, 243, 212),\n        intArrayOf(242, 243, 212),\n        intArrayOf(242, 242, 214),\n        intArrayOf(243, 243, 215),\n        intArrayOf(243, 243, 217),\n        intArrayOf(243, 243, 217),\n        intArrayOf(244, 244, 220),\n        intArrayOf(244, 244, 220),\n        intArrayOf(244, 243, 222),\n        intArrayOf(245, 244, 223),\n        intArrayOf(246, 245, 224),\n        intArrayOf(246, 245, 224),\n        intArrayOf(245, 247, 226),\n        intArrayOf(245, 247, 226),\n        intArrayOf(246, 247, 229),\n        intArrayOf(246, 247, 229),\n        intArrayOf(246, 247, 231),\n        intArrayOf(248, 249, 233),\n        intArrayOf(247, 248, 234),\n        intArrayOf(247, 248, 234),\n        intArrayOf(249, 249, 237),\n        intArrayOf(249, 249, 237),\n        intArrayOf(249, 249, 237),\n        intArrayOf(250, 250, 238),\n        intArrayOf(252, 249, 240),\n        intArrayOf(253, 250, 241),\n        intArrayOf(251, 251, 243),\n        intArrayOf(251, 251, 243),\n        intArrayOf(251, 251, 243),\n        intArrayOf(252, 252, 244),\n        intArrayOf(251, 252, 246),\n        intArrayOf(251, 252, 246),\n        intArrayOf(252, 253, 247),\n        intArrayOf(253, 254, 248),\n        intArrayOf(253, 254, 249),\n        intArrayOf(254, 255, 250),\n        intArrayOf(254, 254, 252),\n        intArrayOf(254, 254, 252),\n        intArrayOf(255, 255, 255),\n        intArrayOf(255, 255, 255)\n    )\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/lut/RainbowLUT.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.lut\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.lut.RainbowLUT\n * @author: Tony Shen\n * @date: 2024/6/17 14:38\n * @version: V1.0 <描述当前版本功能>\n */\n\nobject RainbowLUT {\n    var RAINBOW_LUT = arrayOf(\n        intArrayOf(253, 1, 0),\n        intArrayOf(255, 3, 0),\n        intArrayOf(254, 5, 0),\n        intArrayOf(254, 7, 0),\n        intArrayOf(254, 10, 0),\n        intArrayOf(254, 14, 0),\n        intArrayOf(255, 15, 0),\n        intArrayOf(254, 17, 0),\n        intArrayOf(255, 20, 0),\n        intArrayOf(254, 22, 0),\n        intArrayOf(255, 24, 1),\n        intArrayOf(255, 28, 0),\n        intArrayOf(255, 30, 0),\n        intArrayOf(255, 32, 0),\n        intArrayOf(255, 35, 0),\n        intArrayOf(255, 37, 0),\n        intArrayOf(255, 40, 1),\n        intArrayOf(254, 43, 0),\n        intArrayOf(255, 45, 0),\n        intArrayOf(254, 48, 0),\n        intArrayOf(255, 50, 0),\n        intArrayOf(254, 53, 0),\n        intArrayOf(255, 55, 0),\n        intArrayOf(254, 58, 0),\n        intArrayOf(255, 59, 0),\n        intArrayOf(255, 63, 0),\n        intArrayOf(255, 64, 1),\n        intArrayOf(255, 68, 0),\n        intArrayOf(254, 70, 0),\n        intArrayOf(254, 74, 0),\n        intArrayOf(255, 75, 0),\n        intArrayOf(254, 79, 0),\n        intArrayOf(255, 80, 0),\n        intArrayOf(254, 84, 0),\n        intArrayOf(255, 85, 0),\n        intArrayOf(254, 89, 0),\n        intArrayOf(255, 90, 0),\n        intArrayOf(254, 94, 0),\n        intArrayOf(255, 95, 1),\n        intArrayOf(255, 99, 0),\n        intArrayOf(255, 100, 0),\n        intArrayOf(255, 104, 0),\n        intArrayOf(255, 105, 0),\n        intArrayOf(255, 109, 0),\n        intArrayOf(255, 110, 0),\n        intArrayOf(255, 113, 1),\n        intArrayOf(255, 115, 0),\n        intArrayOf(255, 119, 1),\n        intArrayOf(255, 121, 0),\n        intArrayOf(255, 123, 0),\n        intArrayOf(255, 126, 0),\n        intArrayOf(255, 128, 1),\n        intArrayOf(255, 130, 1),\n        intArrayOf(254, 133, 0),\n        intArrayOf(255, 134, 1),\n        intArrayOf(255, 138, 0),\n        intArrayOf(255, 139, 0),\n        intArrayOf(254, 142, 0),\n        intArrayOf(255, 144, 0),\n        intArrayOf(255, 148, 0),\n        intArrayOf(255, 150, 0),\n        intArrayOf(255, 151, 0),\n        intArrayOf(255, 154, 0),\n        intArrayOf(255, 156, 0),\n        intArrayOf(255, 159, 0),\n        intArrayOf(254, 162, 0),\n        intArrayOf(255, 165, 0),\n        intArrayOf(254, 167, 0),\n        intArrayOf(255, 170, 1),\n        intArrayOf(253, 173, 0),\n        intArrayOf(255, 175, 0),\n        intArrayOf(254, 177, 1),\n        intArrayOf(255, 180, 1),\n        intArrayOf(255, 182, 0),\n        intArrayOf(255, 184, 2),\n        intArrayOf(255, 187, 0),\n        intArrayOf(255, 190, 0),\n        intArrayOf(255, 193, 0),\n        intArrayOf(255, 195, 0),\n        intArrayOf(255, 197, 0),\n        intArrayOf(255, 200, 0),\n        intArrayOf(254, 203, 0),\n        intArrayOf(255, 205, 0),\n        intArrayOf(255, 209, 1),\n        intArrayOf(255, 210, 2),\n        intArrayOf(254, 213, 1),\n        intArrayOf(255, 214, 0),\n        intArrayOf(254, 218, 0),\n        intArrayOf(255, 219, 0),\n        intArrayOf(255, 223, 0),\n        intArrayOf(255, 224, 0),\n        intArrayOf(254, 227, 0),\n        intArrayOf(255, 229, 0),\n        intArrayOf(255, 233, 0),\n        intArrayOf(255, 234, 0),\n        intArrayOf(254, 237, 0),\n        intArrayOf(255, 239, 1),\n        intArrayOf(254, 242, 0),\n        intArrayOf(255, 244, 0),\n        intArrayOf(255, 247, 0),\n        intArrayOf(255, 250, 0),\n        intArrayOf(253, 252, 1),\n        intArrayOf(252, 253, 0),\n        intArrayOf(248, 254, 0),\n        intArrayOf(244, 254, 0),\n        intArrayOf(239, 255, 0),\n        intArrayOf(235, 254, 2),\n        intArrayOf(229, 255, 0),\n        intArrayOf(224, 255, 3),\n        intArrayOf(220, 255, 2),\n        intArrayOf(215, 255, 0),\n        intArrayOf(209, 255, 0),\n        intArrayOf(205, 255, 0),\n        intArrayOf(200, 255, 0),\n        intArrayOf(195, 255, 1),\n        intArrayOf(189, 255, 0),\n        intArrayOf(185, 255, 1),\n        intArrayOf(180, 255, 0),\n        intArrayOf(174, 255, 2),\n        intArrayOf(170, 255, 1),\n        intArrayOf(165, 255, 1),\n        intArrayOf(162, 255, 0),\n        intArrayOf(155, 255, 1),\n        intArrayOf(150, 255, 0),\n        intArrayOf(145, 254, 2),\n        intArrayOf(140, 255, 1),\n        intArrayOf(135, 254, 2),\n        intArrayOf(130, 255, 1),\n        intArrayOf(125, 255, 0),\n        intArrayOf(120, 255, 0),\n        intArrayOf(115, 255, 0),\n        intArrayOf(110, 255, 0),\n        intArrayOf(105, 255, 0),\n        intArrayOf(100, 255, 0),\n        intArrayOf(94, 255, 1),\n        intArrayOf(90, 255, 0),\n        intArrayOf(85, 255, 0),\n        intArrayOf(81, 255, 0),\n        intArrayOf(75, 255, 0),\n        intArrayOf(70, 255, 0),\n        intArrayOf(65, 255, 1),\n        intArrayOf(60, 255, 0),\n        intArrayOf(55, 254, 1),\n        intArrayOf(50, 255, 0),\n        intArrayOf(44, 255, 0),\n        intArrayOf(39, 255, 0),\n        intArrayOf(35, 255, 1),\n        intArrayOf(31, 255, 0),\n        intArrayOf(25, 254, 1),\n        intArrayOf(20, 255, 0),\n        intArrayOf(15, 255, 0),\n        intArrayOf(10, 255, 1),\n        intArrayOf(7, 254, 0),\n        intArrayOf(3, 251, 4),\n        intArrayOf(1, 248, 7),\n        intArrayOf(1, 244, 12),\n        intArrayOf(0, 240, 17),\n        intArrayOf(0, 235, 20),\n        intArrayOf(0, 230, 24),\n        intArrayOf(0, 225, 27),\n        intArrayOf(0, 220, 36),\n        intArrayOf(1, 215, 41),\n        intArrayOf(0, 210, 46),\n        intArrayOf(0, 205, 52),\n        intArrayOf(0, 201, 55),\n        intArrayOf(0, 195, 59),\n        intArrayOf(0, 190, 64),\n        intArrayOf(1, 186, 69),\n        intArrayOf(0, 180, 77),\n        intArrayOf(0, 175, 82),\n        intArrayOf(0, 170, 84),\n        intArrayOf(0, 165, 89),\n        intArrayOf(1, 160, 94),\n        intArrayOf(0, 155, 98),\n        intArrayOf(0, 150, 105),\n        intArrayOf(0, 146, 110),\n        intArrayOf(0, 140, 114),\n        intArrayOf(0, 135, 120),\n        intArrayOf(0, 131, 126),\n        intArrayOf(0, 125, 131),\n        intArrayOf(0, 121, 136),\n        intArrayOf(0, 115, 140),\n        intArrayOf(0, 110, 143),\n        intArrayOf(0, 106, 148),\n        intArrayOf(0, 99, 155),\n        intArrayOf(0, 95, 161),\n        intArrayOf(0, 90, 166),\n        intArrayOf(0, 84, 170),\n        intArrayOf(1, 80, 173),\n        intArrayOf(1, 76, 178),\n        intArrayOf(0, 70, 184),\n        intArrayOf(0, 66, 189),\n        intArrayOf(0, 58, 196),\n        intArrayOf(0, 55, 200),\n        intArrayOf(0, 51, 205),\n        intArrayOf(0, 45, 208),\n        intArrayOf(0, 41, 215),\n        intArrayOf(0, 36, 220),\n        intArrayOf(0, 29, 227),\n        intArrayOf(0, 25, 232),\n        intArrayOf(0, 20, 237),\n        intArrayOf(0, 15, 240),\n        intArrayOf(0, 11, 243),\n        intArrayOf(2, 7, 246),\n        intArrayOf(3, 5, 248),\n        intArrayOf(5, 3, 252),\n        intArrayOf(7, 2, 254),\n        intArrayOf(10, 0, 255),\n        intArrayOf(13, 0, 255),\n        intArrayOf(17, 0, 255),\n        intArrayOf(20, 0, 255),\n        intArrayOf(23, 0, 254),\n        intArrayOf(26, 0, 255),\n        intArrayOf(30, 0, 254),\n        intArrayOf(33, 0, 253),\n        intArrayOf(37, 0, 254),\n        intArrayOf(40, 0, 255),\n        intArrayOf(43, 0, 255),\n        intArrayOf(47, 0, 255),\n        intArrayOf(51, 0, 255),\n        intArrayOf(53, 0, 255),\n        intArrayOf(57, 0, 255),\n        intArrayOf(60, 0, 254),\n        intArrayOf(63, 0, 255),\n        intArrayOf(66, 1, 255),\n        intArrayOf(70, 0, 255),\n        intArrayOf(73, 0, 255),\n        intArrayOf(77, 0, 254),\n        intArrayOf(81, 0, 255),\n        intArrayOf(85, 0, 254),\n        intArrayOf(87, 0, 253),\n        intArrayOf(91, 0, 254),\n        intArrayOf(93, 0, 255),\n        intArrayOf(97, 0, 255),\n        intArrayOf(100, 0, 255),\n        intArrayOf(103, 0, 255),\n        intArrayOf(107, 0, 255),\n        intArrayOf(111, 0, 255),\n        intArrayOf(113, 0, 254),\n        intArrayOf(117, 0, 255),\n        intArrayOf(120, 0, 255),\n        intArrayOf(123, 0, 255),\n        intArrayOf(127, 0, 255),\n        intArrayOf(131, 0, 254),\n        intArrayOf(135, 0, 255),\n        intArrayOf(139, 0, 254),\n        intArrayOf(141, 0, 254),\n        intArrayOf(144, 0, 255),\n        intArrayOf(147, 0, 255),\n        intArrayOf(150, 0, 255),\n        intArrayOf(152, 1, 255),\n        intArrayOf(156, 1, 255),\n        intArrayOf(161, 0, 255),\n        intArrayOf(165, 0, 255),\n        intArrayOf(167, 0, 254),\n        intArrayOf(170, 0, 255)\n    )\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/lut/SpringLUT.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.lut\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.lut.SpringLUT\n * @author: Tony Shen\n * @date: 2024/6/17 14:46\n * @version: V1.0 <描述当前版本功能>\n */\n\nobject SpringLUT {\n    var SPRING_LUT = arrayOf(\n        intArrayOf(254, 0, 255),\n        intArrayOf(255, 1, 255),\n        intArrayOf(255, 1, 255),\n        intArrayOf(255, 3, 252),\n        intArrayOf(255, 3, 251),\n        intArrayOf(255, 5, 248),\n        intArrayOf(255, 6, 248),\n        intArrayOf(255, 7, 247),\n        intArrayOf(255, 8, 245),\n        intArrayOf(255, 9, 246),\n        intArrayOf(254, 10, 245),\n        intArrayOf(255, 12, 244),\n        intArrayOf(254, 12, 244),\n        intArrayOf(254, 13, 242),\n        intArrayOf(255, 14, 241),\n        intArrayOf(255, 14, 240),\n        intArrayOf(255, 16, 239),\n        intArrayOf(255, 17, 237),\n        intArrayOf(255, 18, 236),\n        intArrayOf(255, 18, 236),\n        intArrayOf(255, 20, 235),\n        intArrayOf(255, 21, 234),\n        intArrayOf(255, 22, 235),\n        intArrayOf(255, 23, 231),\n        intArrayOf(254, 25, 232),\n        intArrayOf(254, 25, 229),\n        intArrayOf(255, 26, 230),\n        intArrayOf(255, 27, 228),\n        intArrayOf(255, 29, 227),\n        intArrayOf(255, 29, 227),\n        intArrayOf(255, 30, 226),\n        intArrayOf(255, 30, 225),\n        intArrayOf(255, 31, 225),\n        intArrayOf(255, 33, 222),\n        intArrayOf(254, 34, 222),\n        intArrayOf(255, 35, 219),\n        intArrayOf(254, 36, 219),\n        intArrayOf(255, 38, 217),\n        intArrayOf(253, 38, 217),\n        intArrayOf(254, 40, 216),\n        intArrayOf(255, 39, 214),\n        intArrayOf(255, 40, 215),\n        intArrayOf(255, 41, 214),\n        intArrayOf(255, 43, 213),\n        intArrayOf(255, 43, 213),\n        intArrayOf(255, 45, 210),\n        intArrayOf(255, 46, 210),\n        intArrayOf(255, 48, 208),\n        intArrayOf(254, 48, 208),\n        intArrayOf(254, 49, 206),\n        intArrayOf(255, 50, 205),\n        intArrayOf(255, 50, 203),\n        intArrayOf(255, 51, 204),\n        intArrayOf(255, 52, 201),\n        intArrayOf(255, 54, 202),\n        intArrayOf(255, 54, 200),\n        intArrayOf(255, 56, 199),\n        intArrayOf(255, 56, 198),\n        intArrayOf(255, 58, 199),\n        intArrayOf(255, 59, 195),\n        intArrayOf(255, 61, 196),\n        intArrayOf(255, 61, 194),\n        intArrayOf(255, 62, 193),\n        intArrayOf(255, 63, 192),\n        intArrayOf(255, 65, 191),\n        intArrayOf(255, 65, 189),\n        intArrayOf(255, 67, 188),\n        intArrayOf(255, 67, 188),\n        intArrayOf(253, 68, 187),\n        intArrayOf(254, 69, 186),\n        intArrayOf(253, 70, 186),\n        intArrayOf(254, 72, 183),\n        intArrayOf(255, 71, 183),\n        intArrayOf(255, 73, 181),\n        intArrayOf(255, 74, 181),\n        intArrayOf(255, 75, 180),\n        intArrayOf(255, 76, 178),\n        intArrayOf(255, 77, 179),\n        intArrayOf(254, 78, 177),\n        intArrayOf(255, 79, 177),\n        intArrayOf(254, 80, 177),\n        intArrayOf(255, 82, 174),\n        intArrayOf(255, 83, 175),\n        intArrayOf(255, 83, 172),\n        intArrayOf(255, 83, 172),\n        intArrayOf(255, 85, 169),\n        intArrayOf(255, 86, 169),\n        intArrayOf(255, 86, 167),\n        intArrayOf(255, 88, 166),\n        intArrayOf(255, 88, 166),\n        intArrayOf(255, 90, 166),\n        intArrayOf(255, 91, 164),\n        intArrayOf(254, 92, 165),\n        intArrayOf(254, 93, 161),\n        intArrayOf(255, 94, 162),\n        intArrayOf(255, 95, 159),\n        intArrayOf(255, 96, 160),\n        intArrayOf(255, 97, 158),\n        intArrayOf(255, 98, 157),\n        intArrayOf(255, 98, 156),\n        intArrayOf(255, 100, 157),\n        intArrayOf(255, 101, 155),\n        intArrayOf(255, 103, 154),\n        intArrayOf(255, 103, 152),\n        intArrayOf(255, 105, 151),\n        intArrayOf(255, 105, 150),\n        intArrayOf(255, 106, 148),\n        intArrayOf(255, 107, 147),\n        intArrayOf(255, 109, 148),\n        intArrayOf(255, 109, 146),\n        intArrayOf(255, 109, 145),\n        intArrayOf(255, 110, 146),\n        intArrayOf(255, 111, 144),\n        intArrayOf(255, 113, 143),\n        intArrayOf(254, 114, 143),\n        intArrayOf(255, 115, 141),\n        intArrayOf(254, 116, 139),\n        intArrayOf(255, 118, 136),\n        intArrayOf(254, 119, 136),\n        intArrayOf(255, 120, 135),\n        intArrayOf(255, 120, 134),\n        intArrayOf(255, 121, 135),\n        intArrayOf(255, 122, 133),\n        intArrayOf(255, 122, 131),\n        intArrayOf(255, 124, 132),\n        intArrayOf(255, 125, 131),\n        intArrayOf(255, 126, 130),\n        intArrayOf(255, 127, 128),\n        intArrayOf(254, 129, 127),\n        intArrayOf(254, 129, 125),\n        intArrayOf(255, 130, 124),\n        intArrayOf(255, 131, 123),\n        intArrayOf(255, 132, 124),\n        intArrayOf(255, 132, 122),\n        intArrayOf(255, 134, 121),\n        intArrayOf(255, 134, 121),\n        intArrayOf(255, 136, 120),\n        intArrayOf(255, 136, 119),\n        intArrayOf(255, 138, 120),\n        intArrayOf(255, 140, 117),\n        intArrayOf(255, 141, 115),\n        intArrayOf(255, 142, 112),\n        intArrayOf(255, 142, 112),\n        intArrayOf(255, 143, 111),\n        intArrayOf(255, 143, 109),\n        intArrayOf(255, 145, 110),\n        intArrayOf(255, 145, 108),\n        intArrayOf(255, 147, 108),\n        intArrayOf(254, 148, 108),\n        intArrayOf(255, 149, 107),\n        intArrayOf(255, 150, 105),\n        intArrayOf(255, 151, 104),\n        intArrayOf(255, 151, 103),\n        intArrayOf(255, 153, 102),\n        intArrayOf(255, 154, 100),\n        intArrayOf(255, 155, 99),\n        intArrayOf(255, 156, 99),\n        intArrayOf(255, 157, 98),\n        intArrayOf(254, 158, 97),\n        intArrayOf(255, 159, 98),\n        intArrayOf(254, 160, 96),\n        intArrayOf(254, 161, 94),\n        intArrayOf(255, 162, 95),\n        intArrayOf(255, 162, 92),\n        intArrayOf(255, 164, 91),\n        intArrayOf(255, 164, 87),\n        intArrayOf(255, 166, 88),\n        intArrayOf(255, 167, 87),\n        intArrayOf(255, 169, 86),\n        intArrayOf(255, 169, 86),\n        intArrayOf(255, 171, 85),\n        intArrayOf(255, 171, 83),\n        intArrayOf(254, 173, 84),\n        intArrayOf(254, 173, 82),\n        intArrayOf(255, 174, 82),\n        intArrayOf(255, 175, 80),\n        intArrayOf(255, 177, 79),\n        intArrayOf(255, 177, 77),\n        intArrayOf(255, 178, 77),\n        intArrayOf(255, 178, 77),\n        intArrayOf(254, 180, 75),\n        intArrayOf(255, 181, 74),\n        intArrayOf(254, 182, 74),\n        intArrayOf(255, 183, 72),\n        intArrayOf(254, 184, 72),\n        intArrayOf(255, 186, 69),\n        intArrayOf(255, 186, 69),\n        intArrayOf(255, 187, 68),\n        intArrayOf(254, 188, 66),\n        intArrayOf(255, 189, 67),\n        intArrayOf(255, 189, 66),\n        intArrayOf(255, 191, 65),\n        intArrayOf(255, 192, 63),\n        intArrayOf(255, 193, 62),\n        intArrayOf(254, 194, 61),\n        intArrayOf(255, 196, 60),\n        intArrayOf(254, 196, 60),\n        intArrayOf(254, 197, 56),\n        intArrayOf(254, 199, 57),\n        intArrayOf(254, 199, 55),\n        intArrayOf(255, 200, 55),\n        intArrayOf(255, 200, 53),\n        intArrayOf(255, 202, 54),\n        intArrayOf(255, 202, 50),\n        intArrayOf(255, 204, 51),\n        intArrayOf(255, 204, 50),\n        intArrayOf(255, 207, 49),\n        intArrayOf(255, 207, 47),\n        intArrayOf(254, 209, 48),\n        intArrayOf(254, 209, 45),\n        intArrayOf(255, 210, 46),\n        intArrayOf(255, 211, 42),\n        intArrayOf(255, 212, 43),\n        intArrayOf(255, 212, 41),\n        intArrayOf(255, 214, 40),\n        intArrayOf(255, 214, 40),\n        intArrayOf(255, 215, 39),\n        intArrayOf(255, 217, 38),\n        intArrayOf(254, 217, 38),\n        intArrayOf(255, 219, 35),\n        intArrayOf(254, 220, 35),\n        intArrayOf(255, 222, 33),\n        intArrayOf(255, 222, 33),\n        intArrayOf(255, 223, 30),\n        intArrayOf(254, 224, 30),\n        intArrayOf(255, 225, 29),\n        intArrayOf(255, 226, 28),\n        intArrayOf(255, 227, 29),\n        intArrayOf(255, 228, 27),\n        intArrayOf(255, 229, 26),\n        intArrayOf(255, 230, 26),\n        intArrayOf(255, 231, 24),\n        intArrayOf(254, 232, 24),\n        intArrayOf(255, 234, 21),\n        intArrayOf(254, 235, 21),\n        intArrayOf(254, 235, 19),\n        intArrayOf(255, 236, 19),\n        intArrayOf(255, 237, 20),\n        intArrayOf(254, 238, 18),\n        intArrayOf(254, 239, 16),\n        intArrayOf(254, 241, 15),\n        intArrayOf(254, 241, 13),\n        intArrayOf(255, 242, 13),\n        intArrayOf(255, 243, 11),\n        intArrayOf(255, 244, 12),\n        intArrayOf(255, 244, 8),\n        intArrayOf(255, 246, 9),\n        intArrayOf(255, 247, 8),\n        intArrayOf(255, 249, 7),\n        intArrayOf(255, 249, 5),\n        intArrayOf(255, 251, 6),\n        intArrayOf(255, 252, 3),\n        intArrayOf(254, 253, 3),\n        intArrayOf(254, 253, 2),\n        intArrayOf(254, 254, 0),\n        intArrayOf(255, 255, 1)\n    )\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/lut/SummerLUT.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.lut\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.lut.SummerLUT\n * @author: Tony Shen\n * @date: 2024/6/17 14:51\n * @version: V1.0 <描述当前版本功能>\n */\n\nobject SummerLUT {\n    var SUMMER_LUT = arrayOf(\n        intArrayOf(0, 129, 101),\n        intArrayOf(0, 129, 101),\n        intArrayOf(1, 128, 101),\n        intArrayOf(2, 129, 102),\n        intArrayOf(3, 130, 103),\n        intArrayOf(3, 130, 103),\n        intArrayOf(6, 130, 102),\n        intArrayOf(7, 131, 103),\n        intArrayOf(8, 131, 102),\n        intArrayOf(9, 132, 103),\n        intArrayOf(11, 132, 101),\n        intArrayOf(12, 133, 102),\n        intArrayOf(12, 133, 100),\n        intArrayOf(13, 134, 101),\n        intArrayOf(14, 133, 101),\n        intArrayOf(15, 134, 102),\n        intArrayOf(16, 136, 101),\n        intArrayOf(17, 137, 102),\n        intArrayOf(17, 137, 102),\n        intArrayOf(18, 138, 103),\n        intArrayOf(19, 137, 102),\n        intArrayOf(20, 138, 103),\n        intArrayOf(22, 138, 101),\n        intArrayOf(23, 139, 102),\n        intArrayOf(24, 138, 102),\n        intArrayOf(25, 139, 103),\n        intArrayOf(26, 141, 102),\n        intArrayOf(26, 141, 102),\n        intArrayOf(29, 141, 101),\n        intArrayOf(30, 142, 102),\n        intArrayOf(30, 142, 102),\n        intArrayOf(30, 142, 102),\n        intArrayOf(32, 143, 101),\n        intArrayOf(32, 143, 101),\n        intArrayOf(33, 144, 102),\n        intArrayOf(34, 145, 103),\n        intArrayOf(36, 145, 103),\n        intArrayOf(36, 145, 103),\n        intArrayOf(37, 147, 102),\n        intArrayOf(38, 148, 103),\n        intArrayOf(40, 147, 101),\n        intArrayOf(41, 148, 102),\n        intArrayOf(42, 148, 100),\n        intArrayOf(43, 149, 101),\n        intArrayOf(43, 149, 101),\n        intArrayOf(44, 150, 102),\n        intArrayOf(44, 150, 102),\n        intArrayOf(45, 151, 102),\n        intArrayOf(48, 152, 103),\n        intArrayOf(49, 153, 102),\n        intArrayOf(50, 152, 102),\n        intArrayOf(51, 153, 103),\n        intArrayOf(53, 153, 103),\n        intArrayOf(54, 154, 104),\n        intArrayOf(55, 153, 102),\n        intArrayOf(56, 154, 103),\n        intArrayOf(55, 155, 101),\n        intArrayOf(56, 156, 102),\n        intArrayOf(58, 157, 102),\n        intArrayOf(58, 157, 102),\n        intArrayOf(60, 158, 101),\n        intArrayOf(61, 159, 102),\n        intArrayOf(63, 158, 102),\n        intArrayOf(63, 158, 102),\n        intArrayOf(64, 159, 101),\n        intArrayOf(65, 160, 102),\n        intArrayOf(66, 159, 102),\n        intArrayOf(67, 160, 103),\n        intArrayOf(67, 161, 101),\n        intArrayOf(68, 162, 102),\n        intArrayOf(71, 162, 101),\n        intArrayOf(72, 163, 102),\n        intArrayOf(72, 163, 102),\n        intArrayOf(73, 164, 103),\n        intArrayOf(74, 164, 102),\n        intArrayOf(75, 165, 103),\n        intArrayOf(75, 165, 101),\n        intArrayOf(76, 166, 102),\n        intArrayOf(77, 166, 102),\n        intArrayOf(78, 167, 103),\n        intArrayOf(81, 167, 102),\n        intArrayOf(81, 167, 102),\n        intArrayOf(82, 169, 101),\n        intArrayOf(83, 170, 102),\n        intArrayOf(85, 170, 102),\n        intArrayOf(85, 170, 102),\n        intArrayOf(85, 171, 100),\n        intArrayOf(86, 172, 101),\n        intArrayOf(88, 171, 103),\n        intArrayOf(89, 172, 104),\n        intArrayOf(91, 172, 103),\n        intArrayOf(91, 172, 103),\n        intArrayOf(92, 174, 102),\n        intArrayOf(93, 175, 103),\n        intArrayOf(94, 176, 102),\n        intArrayOf(94, 176, 102),\n        intArrayOf(95, 176, 100),\n        intArrayOf(96, 177, 101),\n        intArrayOf(99, 177, 102),\n        intArrayOf(99, 177, 102),\n        intArrayOf(100, 176, 101),\n        intArrayOf(101, 177, 102),\n        intArrayOf(104, 178, 103),\n        intArrayOf(104, 178, 103),\n        intArrayOf(103, 180, 102),\n        intArrayOf(104, 181, 103),\n        intArrayOf(106, 180, 101),\n        intArrayOf(107, 181, 102),\n        intArrayOf(108, 181, 100),\n        intArrayOf(109, 182, 101),\n        intArrayOf(110, 181, 101),\n        intArrayOf(111, 182, 102),\n        intArrayOf(112, 184, 102),\n        intArrayOf(112, 184, 102),\n        intArrayOf(115, 184, 103),\n        intArrayOf(116, 185, 104),\n        intArrayOf(117, 186, 103),\n        intArrayOf(117, 186, 103),\n        intArrayOf(118, 186, 101),\n        intArrayOf(119, 187, 102),\n        intArrayOf(119, 187, 100),\n        intArrayOf(120, 188, 101),\n        intArrayOf(122, 188, 100),\n        intArrayOf(123, 189, 101),\n        intArrayOf(124, 190, 102),\n        intArrayOf(125, 191, 103),\n        intArrayOf(126, 190, 103),\n        intArrayOf(127, 191, 104),\n        intArrayOf(128, 191, 102),\n        intArrayOf(129, 192, 103),\n        intArrayOf(130, 193, 102),\n        intArrayOf(130, 193, 102),\n        intArrayOf(132, 193, 100),\n        intArrayOf(133, 194, 101),\n        intArrayOf(134, 195, 100),\n        intArrayOf(134, 195, 100),\n        intArrayOf(136, 195, 103),\n        intArrayOf(137, 196, 104),\n        intArrayOf(139, 196, 102),\n        intArrayOf(140, 197, 103),\n        intArrayOf(140, 197, 102),\n        intArrayOf(141, 198, 103),\n        intArrayOf(141, 198, 101),\n        intArrayOf(142, 199, 102),\n        intArrayOf(143, 199, 100),\n        intArrayOf(144, 200, 101),\n        intArrayOf(146, 200, 102),\n        intArrayOf(147, 201, 103),\n        intArrayOf(149, 201, 101),\n        intArrayOf(150, 202, 102),\n        intArrayOf(151, 201, 102),\n        intArrayOf(152, 202, 103),\n        intArrayOf(152, 204, 103),\n        intArrayOf(152, 204, 103),\n        intArrayOf(153, 204, 101),\n        intArrayOf(154, 205, 102),\n        intArrayOf(157, 206, 101),\n        intArrayOf(157, 206, 101),\n        intArrayOf(159, 206, 102),\n        intArrayOf(160, 207, 103),\n        intArrayOf(160, 207, 101),\n        intArrayOf(161, 208, 102),\n        intArrayOf(163, 208, 103),\n        intArrayOf(163, 208, 103),\n        intArrayOf(163, 209, 101),\n        intArrayOf(164, 210, 102),\n        intArrayOf(167, 210, 102),\n        intArrayOf(167, 210, 102),\n        intArrayOf(168, 212, 101),\n        intArrayOf(169, 213, 102),\n        intArrayOf(170, 212, 100),\n        intArrayOf(171, 213, 101),\n        intArrayOf(171, 213, 101),\n        intArrayOf(172, 214, 102),\n        intArrayOf(174, 214, 102),\n        intArrayOf(175, 215, 103),\n        intArrayOf(176, 214, 101),\n        intArrayOf(177, 215, 102),\n        intArrayOf(178, 217, 102),\n        intArrayOf(179, 218, 103),\n        intArrayOf(180, 217, 101),\n        intArrayOf(181, 218, 102),\n        intArrayOf(181, 219, 100),\n        intArrayOf(182, 220, 101),\n        intArrayOf(185, 220, 104),\n        intArrayOf(185, 220, 104),\n        intArrayOf(186, 219, 102),\n        intArrayOf(187, 220, 103),\n        intArrayOf(188, 222, 102),\n        intArrayOf(188, 222, 102),\n        intArrayOf(189, 223, 102),\n        intArrayOf(190, 224, 103),\n        intArrayOf(192, 224, 101),\n        intArrayOf(193, 225, 102),\n        intArrayOf(194, 224, 102),\n        intArrayOf(195, 225, 103),\n        intArrayOf(195, 225, 101),\n        intArrayOf(196, 226, 102),\n        intArrayOf(197, 225, 102),\n        intArrayOf(198, 226, 103),\n        intArrayOf(201, 227, 102),\n        intArrayOf(202, 228, 103),\n        intArrayOf(202, 228, 101),\n        intArrayOf(203, 229, 102),\n        intArrayOf(204, 229, 101),\n        intArrayOf(205, 230, 102),\n        intArrayOf(205, 230, 102),\n        intArrayOf(206, 231, 103),\n        intArrayOf(208, 231, 101),\n        intArrayOf(209, 232, 102),\n        intArrayOf(211, 232, 103),\n        intArrayOf(211, 232, 103),\n        intArrayOf(212, 233, 102),\n        intArrayOf(213, 234, 103),\n        intArrayOf(215, 235, 102),\n        intArrayOf(215, 235, 102),\n        intArrayOf(216, 236, 102),\n        intArrayOf(216, 236, 102),\n        intArrayOf(218, 236, 100),\n        intArrayOf(219, 237, 101),\n        intArrayOf(220, 238, 102),\n        intArrayOf(220, 238, 102),\n        intArrayOf(222, 238, 103),\n        intArrayOf(223, 239, 104),\n        intArrayOf(225, 239, 102),\n        intArrayOf(226, 240, 103),\n        intArrayOf(226, 240, 101),\n        intArrayOf(227, 241, 102),\n        intArrayOf(228, 241, 101),\n        intArrayOf(229, 242, 102),\n        intArrayOf(229, 242, 100),\n        intArrayOf(230, 243, 101),\n        intArrayOf(232, 243, 104),\n        intArrayOf(233, 244, 105),\n        intArrayOf(235, 244, 103),\n        intArrayOf(236, 245, 104),\n        intArrayOf(237, 244, 102),\n        intArrayOf(238, 245, 103),\n        intArrayOf(238, 246, 101),\n        intArrayOf(239, 247, 102),\n        intArrayOf(239, 247, 100),\n        intArrayOf(240, 248, 101),\n        intArrayOf(241, 249, 102),\n        intArrayOf(241, 249, 102),\n        intArrayOf(244, 250, 102),\n        intArrayOf(245, 251, 103),\n        intArrayOf(247, 251, 104),\n        intArrayOf(247, 251, 104),\n        intArrayOf(248, 251, 102),\n        intArrayOf(249, 252, 103),\n        intArrayOf(249, 252, 101),\n        intArrayOf(250, 253, 102),\n        intArrayOf(252, 253, 100),\n        intArrayOf(253, 254, 101),\n        intArrayOf(254, 255, 102),\n        intArrayOf(255, 255, 103)\n    )\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/lut/WinterLUT.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.lut\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.lut.WinterLUT\n * @author: Tony Shen\n * @date: 2024/6/17 15:09\n * @version: V1.0 <描述当前版本功能>\n */\n\nobject WinterLUT {\n    var WINTER_LUT = arrayOf(\n        intArrayOf(0, 0, 254),\n        intArrayOf(1, 1, 255),\n        intArrayOf(0, 2, 253),\n        intArrayOf(0, 4, 253),\n        intArrayOf(0, 4, 253),\n        intArrayOf(1, 5, 252),\n        intArrayOf(0, 6, 250),\n        intArrayOf(1, 7, 251),\n        intArrayOf(0, 8, 251),\n        intArrayOf(0, 9, 252),\n        intArrayOf(0, 10, 250),\n        intArrayOf(0, 11, 249),\n        intArrayOf(0, 12, 249),\n        intArrayOf(0, 13, 249),\n        intArrayOf(0, 14, 247),\n        intArrayOf(1, 15, 246),\n        intArrayOf(0, 17, 247),\n        intArrayOf(0, 17, 245),\n        intArrayOf(0, 19, 246),\n        intArrayOf(0, 19, 246),\n        intArrayOf(1, 20, 246),\n        intArrayOf(1, 20, 246),\n        intArrayOf(0, 22, 245),\n        intArrayOf(0, 22, 243),\n        intArrayOf(0, 24, 244),\n        intArrayOf(0, 25, 242),\n        intArrayOf(0, 27, 241),\n        intArrayOf(0, 27, 241),\n        intArrayOf(0, 29, 241),\n        intArrayOf(0, 29, 241),\n        intArrayOf(0, 30, 240),\n        intArrayOf(0, 30, 238),\n        intArrayOf(0, 32, 239),\n        intArrayOf(1, 33, 238),\n        intArrayOf(0, 34, 238),\n        intArrayOf(0, 35, 238),\n        intArrayOf(0, 35, 238),\n        intArrayOf(1, 37, 237),\n        intArrayOf(0, 38, 235),\n        intArrayOf(1, 39, 234),\n        intArrayOf(0, 40, 234),\n        intArrayOf(1, 41, 234),\n        intArrayOf(0, 42, 232),\n        intArrayOf(0, 43, 233),\n        intArrayOf(0, 44, 233),\n        intArrayOf(0, 45, 234),\n        intArrayOf(0, 45, 232),\n        intArrayOf(1, 47, 231),\n        intArrayOf(0, 48, 232),\n        intArrayOf(0, 49, 230),\n        intArrayOf(0, 51, 230),\n        intArrayOf(0, 51, 228),\n        intArrayOf(1, 52, 229),\n        intArrayOf(1, 53, 227),\n        intArrayOf(1, 55, 226),\n        intArrayOf(1, 55, 226),\n        intArrayOf(0, 56, 227),\n        intArrayOf(0, 56, 227),\n        intArrayOf(0, 58, 227),\n        intArrayOf(0, 59, 225),\n        intArrayOf(0, 60, 226),\n        intArrayOf(0, 61, 224),\n        intArrayOf(0, 62, 223),\n        intArrayOf(0, 62, 221),\n        intArrayOf(0, 64, 222),\n        intArrayOf(0, 65, 221),\n        intArrayOf(0, 66, 222),\n        intArrayOf(1, 67, 223),\n        intArrayOf(1, 68, 221),\n        intArrayOf(1, 68, 219),\n        intArrayOf(0, 70, 220),\n        intArrayOf(1, 71, 219),\n        intArrayOf(0, 72, 218),\n        intArrayOf(1, 73, 219),\n        intArrayOf(0, 74, 217),\n        intArrayOf(0, 75, 218),\n        intArrayOf(0, 76, 216),\n        intArrayOf(0, 77, 217),\n        intArrayOf(0, 77, 215),\n        intArrayOf(1, 78, 216),\n        intArrayOf(0, 80, 215),\n        intArrayOf(0, 81, 216),\n        intArrayOf(0, 83, 215),\n        intArrayOf(0, 83, 215),\n        intArrayOf(0, 83, 213),\n        intArrayOf(1, 84, 214),\n        intArrayOf(1, 86, 213),\n        intArrayOf(1, 86, 211),\n        intArrayOf(0, 88, 212),\n        intArrayOf(0, 88, 211),\n        intArrayOf(0, 90, 210),\n        intArrayOf(0, 90, 210),\n        intArrayOf(0, 93, 209),\n        intArrayOf(0, 93, 209),\n        intArrayOf(0, 94, 208),\n        intArrayOf(0, 94, 207),\n        intArrayOf(0, 96, 208),\n        intArrayOf(1, 97, 207),\n        intArrayOf(0, 98, 207),\n        intArrayOf(1, 99, 206),\n        intArrayOf(1, 99, 206),\n        intArrayOf(2, 101, 205),\n        intArrayOf(0, 102, 203),\n        intArrayOf(1, 103, 203),\n        intArrayOf(0, 104, 203),\n        intArrayOf(1, 105, 202),\n        intArrayOf(0, 106, 200),\n        intArrayOf(0, 107, 201),\n        intArrayOf(0, 108, 201),\n        intArrayOf(0, 109, 202),\n        intArrayOf(0, 109, 200),\n        intArrayOf(1, 111, 200),\n        intArrayOf(0, 111, 200),\n        intArrayOf(1, 113, 199),\n        intArrayOf(0, 114, 197),\n        intArrayOf(0, 115, 196),\n        intArrayOf(1, 116, 197),\n        intArrayOf(1, 116, 196),\n        intArrayOf(1, 118, 195),\n        intArrayOf(1, 118, 195),\n        intArrayOf(0, 120, 196),\n        intArrayOf(0, 120, 196),\n        intArrayOf(0, 122, 195),\n        intArrayOf(0, 123, 193),\n        intArrayOf(0, 124, 194),\n        intArrayOf(0, 125, 192),\n        intArrayOf(1, 126, 192),\n        intArrayOf(1, 126, 190),\n        intArrayOf(0, 128, 191),\n        intArrayOf(0, 128, 189),\n        intArrayOf(0, 130, 190),\n        intArrayOf(0, 130, 190),\n        intArrayOf(1, 131, 189),\n        intArrayOf(2, 132, 190),\n        intArrayOf(0, 133, 189),\n        intArrayOf(1, 135, 188),\n        intArrayOf(0, 136, 188),\n        intArrayOf(1, 137, 187),\n        intArrayOf(0, 138, 185),\n        intArrayOf(1, 139, 186),\n        intArrayOf(0, 140, 185),\n        intArrayOf(0, 141, 186),\n        intArrayOf(0, 141, 184),\n        intArrayOf(0, 143, 183),\n        intArrayOf(0, 143, 183),\n        intArrayOf(0, 145, 182),\n        intArrayOf(0, 145, 182),\n        intArrayOf(0, 147, 181),\n        intArrayOf(0, 149, 182),\n        intArrayOf(0, 149, 181),\n        intArrayOf(0, 151, 180),\n        intArrayOf(0, 151, 178),\n        intArrayOf(1, 152, 179),\n        intArrayOf(1, 153, 177),\n        intArrayOf(0, 155, 177),\n        intArrayOf(0, 155, 177),\n        intArrayOf(0, 156, 178),\n        intArrayOf(0, 156, 178),\n        intArrayOf(1, 158, 177),\n        intArrayOf(0, 159, 175),\n        intArrayOf(0, 160, 176),\n        intArrayOf(0, 161, 174),\n        intArrayOf(0, 162, 173),\n        intArrayOf(0, 163, 172),\n        intArrayOf(0, 164, 173),\n        intArrayOf(0, 165, 171),\n        intArrayOf(1, 166, 170),\n        intArrayOf(2, 167, 171),\n        intArrayOf(0, 168, 171),\n        intArrayOf(1, 169, 172),\n        intArrayOf(0, 170, 170),\n        intArrayOf(1, 171, 170),\n        intArrayOf(0, 172, 170),\n        intArrayOf(0, 173, 169),\n        intArrayOf(0, 174, 167),\n        intArrayOf(0, 176, 166),\n        intArrayOf(0, 176, 166),\n        intArrayOf(0, 178, 166),\n        intArrayOf(0, 178, 166),\n        intArrayOf(0, 178, 166),\n        intArrayOf(0, 180, 165),\n        intArrayOf(0, 181, 166),\n        intArrayOf(0, 183, 165),\n        intArrayOf(0, 183, 163),\n        intArrayOf(0, 185, 164),\n        intArrayOf(0, 185, 162),\n        intArrayOf(0, 187, 162),\n        intArrayOf(0, 187, 162),\n        intArrayOf(0, 189, 161),\n        intArrayOf(0, 189, 161),\n        intArrayOf(0, 191, 160),\n        intArrayOf(0, 191, 158),\n        intArrayOf(0, 193, 159),\n        intArrayOf(0, 193, 158),\n        intArrayOf(0, 194, 159),\n        intArrayOf(0, 194, 157),\n        intArrayOf(0, 196, 158),\n        intArrayOf(0, 196, 156),\n        intArrayOf(0, 198, 155),\n        intArrayOf(0, 199, 153),\n        intArrayOf(0, 200, 154),\n        intArrayOf(0, 202, 154),\n        intArrayOf(0, 203, 152),\n        intArrayOf(0, 204, 153),\n        intArrayOf(0, 204, 153),\n        intArrayOf(1, 205, 154),\n        intArrayOf(0, 206, 152),\n        intArrayOf(0, 207, 151),\n        intArrayOf(0, 208, 151),\n        intArrayOf(0, 209, 151),\n        intArrayOf(0, 210, 149),\n        intArrayOf(1, 211, 148),\n        intArrayOf(0, 212, 148),\n        intArrayOf(0, 213, 147),\n        intArrayOf(0, 214, 146),\n        intArrayOf(0, 215, 147),\n        intArrayOf(0, 217, 148),\n        intArrayOf(0, 217, 148),\n        intArrayOf(0, 219, 147),\n        intArrayOf(0, 219, 145),\n        intArrayOf(0, 220, 146),\n        intArrayOf(0, 221, 144),\n        intArrayOf(0, 223, 143),\n        intArrayOf(0, 223, 142),\n        intArrayOf(0, 225, 143),\n        intArrayOf(0, 225, 141),\n        intArrayOf(0, 226, 140),\n        intArrayOf(0, 226, 140),\n        intArrayOf(0, 228, 141),\n        intArrayOf(0, 228, 141),\n        intArrayOf(0, 230, 140),\n        intArrayOf(0, 231, 139),\n        intArrayOf(0, 232, 140),\n        intArrayOf(0, 234, 139),\n        intArrayOf(0, 235, 137),\n        intArrayOf(0, 236, 136),\n        intArrayOf(0, 236, 136),\n        intArrayOf(1, 237, 136),\n        intArrayOf(0, 238, 136),\n        intArrayOf(0, 239, 135),\n        intArrayOf(0, 240, 135),\n        intArrayOf(0, 241, 134),\n        intArrayOf(0, 242, 132),\n        intArrayOf(1, 243, 133),\n        intArrayOf(0, 244, 133),\n        intArrayOf(1, 245, 134),\n        intArrayOf(0, 246, 132),\n        intArrayOf(0, 247, 132),\n        intArrayOf(0, 249, 133),\n        intArrayOf(0, 249, 131),\n        intArrayOf(0, 251, 130),\n        intArrayOf(0, 251, 128),\n        intArrayOf(0, 252, 129),\n        intArrayOf(0, 253, 128),\n        intArrayOf(0, 254, 129),\n        intArrayOf(0, 254, 129)\n    )\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/math/FFT.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.math\n\nimport kotlin.math.max\nimport kotlin.math.sin\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.math.FFT\n * @author: Tony Shen\n * @date: 2025/3/14 15:36\n * @version: V1.0 <描述当前版本功能>\n */\nclass FFT(logN: Int) {\n    // Weighting factors\n    protected var w1: FloatArray\n    protected var w2: FloatArray\n    protected var w3: FloatArray\n\n    init {\n        // Prepare the weighting factors\n        w1 = FloatArray(logN)\n        w2 = FloatArray(logN)\n        w3 = FloatArray(logN)\n        var N = 1\n        for (k in 0..<logN) {\n            N = N shl 1\n            val angle = -2.0 * Math.PI / N\n            w1[k] = sin(0.5 * angle).toFloat()\n            w2[k] = -2.0f * w1[k] * w1[k]\n            w3[k] = sin(angle).toFloat()\n        }\n    }\n\n    private fun scramble(n: Int, real: FloatArray, imag: FloatArray) {\n        var j = 0\n\n        for (i in 0..<n) {\n            if (i > j) {\n                var t = real[j]\n                real[j] = real[i]\n                real[i] = t\n                t = imag[j]\n                imag[j] = imag[i]\n                imag[i] = t\n            }\n            var m = n shr 1\n            while (j >= m && m >= 2) {\n                j -= m\n                m = m shr 1\n            }\n            j += m\n        }\n    }\n\n    private fun butterflies(n: Int, logN: Int, direction: Int, real: FloatArray, imag: FloatArray) {\n        var N = 1\n\n        for (k in 0..<logN) {\n            var w_re: Float\n            var w_im: Float\n            var temp_re: Float\n            var temp_im: Float\n            var wt: Float\n            val half_N = N\n            N = N shl 1\n            wt = direction * w1[k]\n            val wp_re = w2[k]\n            val wp_im = direction * w3[k]\n            w_re = 1.0f\n            w_im = 0.0f\n            for (offset in 0..<half_N) {\n                var i = offset\n                while (i < n) {\n                    val j = i + half_N\n                    val re = real[j]\n                    val im = imag[j]\n                    temp_re = (w_re * re) - (w_im * im)\n                    temp_im = (w_im * re) + (w_re * im)\n                    real[j] = real[i] - temp_re\n                    real[i] += temp_re\n                    imag[j] = imag[i] - temp_im\n                    imag[i] += temp_im\n                    i += N\n                }\n                wt = w_re\n                w_re = wt * wp_re - w_im * wp_im + w_re\n                w_im = w_im * wp_re + wt * wp_im + w_im\n            }\n        }\n        if (direction == -1) {\n            val nr = 1.0f / n\n            for (i in 0..<n) {\n                real[i] *= nr\n                imag[i] *= nr\n            }\n        }\n    }\n\n    fun transform1D(real: FloatArray, imag: FloatArray, logN: Int, n: Int, forward: Boolean) {\n        scramble(n, real, imag)\n        butterflies(n, logN, if (forward) 1 else -1, real, imag)\n    }\n\n    fun transform2D(real: FloatArray, imag: FloatArray, cols: Int, rows: Int, forward: Boolean) {\n        val log2cols = log2(cols)\n        val log2rows = log2(rows)\n        val n = max(rows.toDouble(), cols.toDouble()).toInt()\n        val rtemp = FloatArray(n)\n        val itemp = FloatArray(n)\n\n        // FFT the rows\n        for (y in 0..<rows) {\n            val offset = y * cols\n            System.arraycopy(real, offset, rtemp, 0, cols)\n            System.arraycopy(imag, offset, itemp, 0, cols)\n            transform1D(rtemp, itemp, log2cols, cols, forward)\n            System.arraycopy(rtemp, 0, real, offset, cols)\n            System.arraycopy(itemp, 0, imag, offset, cols)\n        }\n\n        // FFT the columns\n        for (x in 0..<cols) {\n            var index = x\n            for (y in 0..<rows) {\n                rtemp[y] = real[index]\n                itemp[y] = imag[index]\n                index += cols\n            }\n            transform1D(rtemp, itemp, log2rows, rows, forward)\n            index = x\n            for (y in 0..<rows) {\n                real[index] = rtemp[y]\n                imag[index] = itemp[y]\n                index += cols\n            }\n        }\n    }\n\n    private fun log2(n: Int): Int {\n        var m = 1\n        var log2n = 0\n\n        while (m < n) {\n            m *= 2\n            log2n++\n        }\n        return if (m == n) log2n else -1\n    }\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/math/Functions.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.math\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.math.Functions\n * @author: Tony Shen\n * @date: 2025/3/10 11:49\n * @version: V1.0 <描述当前版本功能>\n */\ninterface Function1D {\n    fun evaluate(x: Float): Float\n}\n\ninterface Function2D {\n    fun evaluate(x: Float, y: Float): Float\n}\n\ninterface Function3D {\n    fun evaluate(x: Float, y: Float, z: Float): Float\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/math/ImageMath.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.math\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.utils.ImageMath\n * @author: Tony Shen\n * @date:  2025/3/8 15:43\n * @version: V1.0 <描述当前版本功能>\n */\n\nval PI: Float = Math.PI.toFloat()\n\nval HALF_PI = Math.PI.toFloat() / 2.0f\n\nval TWO_PI = Math.PI.toFloat() * 2.0f\n\n/**\n * Return a mod b. This differs from the % operator with respect to negative numbers.\n * @param a the dividend\n * @param b the divisor\n * @return a mod b\n */\nfun mod(a: Double, b: Double): Double {\n    var a = a\n    val n = (a / b).toInt()\n\n    a -= n * b\n    if (a < 0) return a + b\n    return a\n}\n\n/**\n * Return a mod b. This differs from the % operator with respect to negative numbers.\n * @param a the dividend\n * @param b the divisor\n * @return a mod b\n */\nfun mod(a: Float, b: Float): Float {\n    var a = a\n    val n = (a / b).toInt()\n\n    a -= n * b\n    if (a < 0) return a + b\n    return a\n}\n\n/**\n * Return a mod b. This differs from the % operator with respect to negative numbers.\n * @param a the dividend\n * @param b the divisor\n * @return a mod b\n */\nfun mod(a: Int, b: Int): Int {\n    var a = a\n    val n = a / b\n\n    a -= n * b\n    if (a < 0) return a + b\n    return a\n}\n\n/**\n * Linear interpolation of ARGB values.\n * @param t the interpolation parameter\n * @param rgb1 the lower interpolation range\n * @param rgb2 the upper interpolation range\n * @return the interpolated value\n */\nfun mixColors(t: Float, rgb1: Int, rgb2: Int): Int {\n    var a1 = (rgb1 shr 24) and 0xff\n    var r1 = (rgb1 shr 16) and 0xff\n    var g1 = (rgb1 shr 8) and 0xff\n    var b1 = rgb1 and 0xff\n    val a2 = (rgb2 shr 24) and 0xff\n    val r2 = (rgb2 shr 16) and 0xff\n    val g2 = (rgb2 shr 8) and 0xff\n    val b2 = rgb2 and 0xff\n    a1 = lerp(t, a1, a2)\n    r1 = lerp(t, r1, r2)\n    g1 = lerp(t, g1, g2)\n    b1 = lerp(t, b1, b2)\n    return (a1 shl 24) or (r1 shl 16) or (g1 shl 8) or b1\n}\n\n/**\n * Linear interpolation.\n * @param t the interpolation parameter\n * @param a the lower interpolation range\n * @param b the upper interpolation range\n * @return the interpolated value\n */\nfun lerp(t: Float, a: Int, b: Int): Int {\n    return (a + t * (b - a)).toInt()\n}\n\n/**\n * Bilinear interpolation of ARGB values.\n * @param x the X interpolation parameter 0..1\n * @param y the y interpolation parameter 0..1\n * @param rgb array of four ARGB values in the order NW, NE, SW, SE\n * @return the interpolated value\n */\nfun bilinearInterpolate(x: Float, y: Float, nw: Int, ne: Int, sw: Int, se: Int): Int {\n    var m0: Float\n    var m1: Float\n    val a0 = (nw shr 24) and 0xff\n    val r0 = (nw shr 16) and 0xff\n    val g0 = (nw shr 8) and 0xff\n    val b0 = nw and 0xff\n    val a1 = (ne shr 24) and 0xff\n    val r1 = (ne shr 16) and 0xff\n    val g1 = (ne shr 8) and 0xff\n    val b1 = ne and 0xff\n    val a2 = (sw shr 24) and 0xff\n    val r2 = (sw shr 16) and 0xff\n    val g2 = (sw shr 8) and 0xff\n    val b2 = sw and 0xff\n    val a3 = (se shr 24) and 0xff\n    val r3 = (se shr 16) and 0xff\n    val g3 = (se shr 8) and 0xff\n    val b3 = se and 0xff\n\n    val cx = 1.0f - x\n    val cy = 1.0f - y\n\n    m0 = cx * a0 + x * a1\n    m1 = cx * a2 + x * a3\n    val a = (cy * m0 + y * m1).toInt()\n\n    m0 = cx * r0 + x * r1\n    m1 = cx * r2 + x * r3\n    val r = (cy * m0 + y * m1).toInt()\n\n    m0 = cx * g0 + x * g1\n    m1 = cx * g2 + x * g3\n    val g = (cy * m0 + y * m1).toInt()\n\n    m0 = cx * b0 + x * b1\n    m1 = cx * b2 + x * b3\n    val b = (cy * m0 + y * m1).toInt()\n\n    return (a shl 24) or (r shl 16) or (g shl 8) or b\n}\n\n\n/**\n * A smoothed step function. A cubic function is used to smooth the step between two thresholds.\n * @param a the lower threshold position\n * @param b the upper threshold position\n * @param x the input parameter\n * @return the output value\n */\nfun smoothStep(a: Float, b: Float, x: Float): Float {\n    var x = x\n    if (x < a) return 0f\n    if (x >= b) return 1f\n    x = (x - a) / (b - a)\n    return x * x * (3 - 2 * x)\n}\n\n/**\n * The triangle function. Returns a repeating triangle shape in the range 0..1 with wavelength 1.0\n * @param x the input parameter\n * @return the output value\n */\nfun triangle(x: Float): Float {\n    val r = mod(x, 1.0f)\n    return 2.0f * (if (r < 0.5) r else 1 - r)\n}\n\n\n/**\n * Apply a bias to a number in the unit interval, moving numbers towards 0 or 1\n * according to the bias parameter.\n * @param a the number to bias\n * @param b the bias parameter. 0.5 means no change, smaller values bias towards 0, larger towards 1.\n * @return the output value\n */\nfun bias(a: Float, b: Float): Float {\n    return a / ((1.0f / b - 2) * (1.0f - a) + 1)\n}\n\n/**\n * A variant of the gamma function.\n * @param a the number to apply gain to\n * @param b the gain parameter. 0.5 means no change, smaller values reduce gain, larger values increase gain.\n * @return the output value\n */\nfun gain(a: Float, b: Float): Float {\n    val c = (1.0f / b - 2.0f) * (1.0f - 2.0f * a)\n    return if (a < 0.5) a / (c + 1.0f)\n    else (c - a) / (c - 1.0f)\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/math/Noise.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.math\n\nimport java.util.*\nimport kotlin.math.abs\nimport kotlin.math.max\nimport kotlin.math.min\nimport kotlin.math.sqrt\n\n/**\n * Perlin Noise functions\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.math.Noise\n * @author: Tony Shen\n * @date: 2025/3/10 11:48\n * @version: V1.0 <描述当前版本功能>\n */\nclass Noise : Function1D, Function2D, Function3D {\n\n    override fun evaluate(x: Float): Float {\n        return noise1(x)\n    }\n\n    override fun evaluate(x: Float, y: Float): Float {\n        return noise2(x, y)\n    }\n\n    override fun evaluate(x: Float, y: Float, z: Float): Float {\n        return noise3(x, y, z)\n    }\n\n    companion object {\n        private val randomGenerator: Random = Random()\n\n        /**\n         * Compute turbulence using Perlin noise.\n         * @param x the x value\n         * @param y the y value\n         * @param octaves number of octaves of turbulence\n         * @return turbulence value at (x,y)\n         */\n        fun turbulence2(x: Float, y: Float, octaves: Float): Float {\n            var t = 0.0f\n\n            var f = 1.0f\n            while (f <= octaves) {\n                t += abs(noise2(f * x, f * y)) / f\n                f *= 2f\n            }\n            return t\n        }\n\n        /**\n         * Compute turbulence using Perlin noise.\n         * @param x the x value\n         * @param y the y value\n         * @param octaves number of octaves of turbulence\n         * @return turbulence value at (x,y)\n         */\n        fun turbulence3(x: Float, y: Float, z: Float, octaves: Float): Float {\n            var t = 0.0f\n\n            var f = 1.0f\n            while (f <= octaves) {\n                t += abs(noise3(f * x, f * y, f * z)) / f\n                f *= 2f\n            }\n            return t\n        }\n\n        internal const val B = 0x100\n        private const val BM = 0xff\n        private const val N = 0x1000\n\n        var p: IntArray = IntArray(B + B + 2)\n        var g3: Array<FloatArray> = Array(B + B + 2) { FloatArray(3) }\n        var g2: Array<FloatArray> = Array(B + B + 2) { FloatArray(2) }\n        var g1: FloatArray = FloatArray(B + B + 2)\n        var start: Boolean = true\n\n        private fun sCurve(t: Float): Float {\n            return t * t * (3.0f - 2.0f * t)\n        }\n\n        /**\n         * Compute 1-dimensional Perlin noise.\n         * @param x the x value\n         * @return noise value at x in the range -1..1\n         */\n        fun noise1(x: Float): Float {\n            val bx0: Int\n            val rx0: Float\n            val v: Float\n\n            if (start) {\n                start = false\n                init()\n            }\n\n            val t = x + N\n            bx0 = (t.toInt()) and BM\n            val bx1 = (bx0 + 1) and BM\n            rx0 = t - t.toInt()\n            val rx1 = rx0 - 1.0f\n\n            val sx = sCurve(rx0)\n\n            val u = rx0 * g1[p[bx0]]\n            v = rx1 * g1[p[bx1]]\n            return 2.3f * lerp(sx, u, v)\n        }\n\n        /**\n         * Compute 2-dimensional Perlin noise.\n         * @param x the x coordinate\n         * @param y the y coordinate\n         * @return noise value at (x,y)\n         */\n        fun noise2(x: Float, y: Float): Float {\n            val bx0: Int\n            val by0: Int\n            val b00: Int\n            val b10: Int\n            val b01: Int\n            val b11: Int\n            val rx0: Float\n            val ry0: Float\n            val a: Float\n            val b: Float\n            var u: Float\n            var v: Float\n            val j: Int\n\n            if (start) {\n                start = false\n                init()\n            }\n\n            var t = x + N\n            bx0 = (t.toInt()) and BM\n            val bx1 = (bx0 + 1) and BM\n            rx0 = t - t.toInt()\n            val rx1 = rx0 - 1.0f\n\n            t = y + N\n            by0 = (t.toInt()) and BM\n            val by1 = (by0 + 1) and BM\n            ry0 = t - t.toInt()\n            val ry1 = ry0 - 1.0f\n\n            val i = p[bx0]\n            j = p[bx1]\n\n            b00 = p[i + by0]\n            b10 = p[j + by0]\n            b01 = p[i + by1]\n            b11 = p[j + by1]\n\n            val sx = sCurve(rx0)\n            val sy = sCurve(ry0)\n\n            var q = g2[b00]\n            u = rx0 * q[0] + ry0 * q[1]\n            q = g2[b10]\n            v = rx1 * q[0] + ry0 * q[1]\n            a = lerp(sx, u, v)\n\n            q = g2[b01]\n            u = rx0 * q[0] + ry1 * q[1]\n            q = g2[b11]\n            v = rx1 * q[0] + ry1 * q[1]\n            b = lerp(sx, u, v)\n\n            return 1.5f * lerp(sy, a, b)\n        }\n\n        /**\n         * Compute 3-dimensional Perlin noise.\n         * @param x the x coordinate\n         * @param y the y coordinate\n         * @param y the y coordinate\n         * @return noise value at (x,y,z)\n         */\n        fun noise3(x: Float, y: Float, z: Float): Float {\n            val bx0: Int\n            val by0: Int\n            val bz0: Int\n            val b00: Int\n            val b10: Int\n            val b01: Int\n            val b11: Int\n            val rx0: Float\n            val ry0: Float\n            val rz0: Float\n            var a: Float\n            var b: Float\n            val c: Float\n            val d: Float\n            var u: Float\n            var v: Float\n            val j: Int\n\n            if (start) {\n                start = false\n                init()\n            }\n\n            var t = x + N\n            bx0 = (t.toInt()) and BM\n            val bx1 = (bx0 + 1) and BM\n            rx0 = t - t.toInt()\n            val rx1 = rx0 - 1.0f\n\n            t = y + N\n            by0 = (t.toInt()) and BM\n            val by1 = (by0 + 1) and BM\n            ry0 = t - t.toInt()\n            val ry1 = ry0 - 1.0f\n\n            t = z + N\n            bz0 = (t.toInt()) and BM\n            val bz1 = (bz0 + 1) and BM\n            rz0 = t - t.toInt()\n            val rz1 = rz0 - 1.0f\n\n            val i = p[bx0]\n            j = p[bx1]\n\n            b00 = p[i + by0]\n            b10 = p[j + by0]\n            b01 = p[i + by1]\n            b11 = p[j + by1]\n\n            t = sCurve(rx0)\n            val sy = sCurve(ry0)\n            val sz = sCurve(rz0)\n\n            var q = g3[b00 + bz0]\n            u = rx0 * q[0] + ry0 * q[1] + rz0 * q[2]\n            q = g3[b10 + bz0]\n            v = rx1 * q[0] + ry0 * q[1] + rz0 * q[2]\n            a = lerp(t, u, v)\n\n            q = g3[b01 + bz0]\n            u = rx0 * q[0] + ry1 * q[1] + rz0 * q[2]\n            q = g3[b11 + bz0]\n            v = rx1 * q[0] + ry1 * q[1] + rz0 * q[2]\n            b = lerp(t, u, v)\n\n            c = lerp(sy, a, b)\n\n            q = g3[b00 + bz1]\n            u = rx0 * q[0] + ry0 * q[1] + rz1 * q[2]\n            q = g3[b10 + bz1]\n            v = rx1 * q[0] + ry0 * q[1] + rz1 * q[2]\n            a = lerp(t, u, v)\n\n            q = g3[b01 + bz1]\n            u = rx0 * q[0] + ry1 * q[1] + rz1 * q[2]\n            q = g3[b11 + bz1]\n            v = rx1 * q[0] + ry1 * q[1] + rz1 * q[2]\n            b = lerp(t, u, v)\n\n            d = lerp(sy, a, b)\n\n            return 1.5f * lerp(sz, c, d)\n        }\n\n        fun lerp(t: Float, a: Float, b: Float): Float {\n            return a + t * (b - a)\n        }\n\n        private fun normalize2(v: FloatArray) {\n            val s = sqrt((v[0] * v[0] + v[1] * v[1]).toDouble()).toFloat()\n            v[0] = v[0] / s\n            v[1] = v[1] / s\n        }\n\n        fun normalize3(v: FloatArray) {\n            val s = sqrt((v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).toDouble()).toFloat()\n            v[0] = v[0] / s\n            v[1] = v[1] / s\n            v[2] = v[2] / s\n        }\n\n        private fun random(): Int {\n            return randomGenerator.nextInt() and 0x7fffffff\n        }\n\n        private fun init() {\n            var j: Int\n            var k: Int\n\n            var i = 0\n            while (i < B) {\n                p[i] = i\n\n                g1[i] = ((random() % (B + B)) - B).toFloat() / B\n\n                j = 0\n                while (j < 2) {\n                    g2[i][j] = ((random() % (B + B)) - B).toFloat() / B\n                    j++\n                }\n                normalize2(g2[i])\n\n                j = 0\n                while (j < 3) {\n                    g3[i][j] = ((random() % (B + B)) - B).toFloat() / B\n                    j++\n                }\n                normalize3(g3[i])\n                i++\n            }\n\n            i = B - 1\n            while (i >= 0) {\n                k = p[i]\n                p[i] = p[(random() % B).also { j = it }]\n                p[j] = k\n                i--\n            }\n\n            i = 0\n            while (i < B + 2) {\n                p[B + i] = p[i]\n                g1[B + i] = g1[i]\n                j = 0\n                while (j < 2) {\n                    g2[B + i][j] = g2[i][j]\n                    j++\n                }\n                j = 0\n                while (j < 3) {\n                    g3[B + i][j] = g3[i][j]\n                    j++\n                }\n                i++\n            }\n        }\n\n        /**\n         * Returns the minimum and maximum of a number of random values\n         * of the given function. This is useful for making some stab at\n         * normalising the function.\n         */\n        fun findRange(f: Function1D, minmax: FloatArray?): FloatArray {\n            var minmax = minmax\n            if (minmax == null) minmax = FloatArray(2)\n            var min = 0f\n            var max = 0f\n            // Some random numbers here...\n            var x = -100f\n            while (x < 100) {\n                val n: Float = f.evaluate(x)\n                min = min(min.toDouble(), n.toDouble()).toFloat()\n                max = max(max.toDouble(), n.toDouble()).toFloat()\n                x += 1.27139.toFloat()\n            }\n            minmax[0] = min\n            minmax[1] = max\n            return minmax\n        }\n\n        /**\n         * Returns the minimum and maximum of a number of random values\n         * of the given function. This is useful for making some stab at\n         * normalising the function.\n         */\n        fun findRange(f: Function2D, minmax: FloatArray?): FloatArray {\n            var minmax = minmax\n            if (minmax == null) minmax = FloatArray(2)\n            var min = 0f\n            var max = 0f\n            // Some random numbers here...\n            var y = -100f\n            while (y < 100) {\n                var x = -100f\n                while (x < 100) {\n                    val n: Float = f.evaluate(x, y)\n                    min = min(min.toDouble(), n.toDouble()).toFloat()\n                    max = max(max.toDouble(), n.toDouble()).toFloat()\n                    x += 10.77139.toFloat()\n                }\n                y += 10.35173.toFloat()\n            }\n            minmax[0] = min\n            minmax[1] = max\n            return minmax\n        }\n    }\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/utils/ImageUtils.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.utils\n\nimport org.apache.batik.anim.dom.SAXSVGDocumentFactory\nimport org.apache.batik.transcoder.TranscoderInput\nimport org.apache.batik.transcoder.TranscoderOutput\nimport org.apache.batik.transcoder.image.ImageTranscoder\nimport org.apache.batik.util.XMLResourceDescriptor\nimport org.w3c.dom.Document\nimport org.w3c.dom.Element\nimport java.awt.image.BufferedImage\nimport java.io.*\nimport javax.imageio.ImageIO\nimport javax.xml.transform.TransformerFactory\nimport javax.xml.transform.dom.DOMSource\nimport javax.xml.transform.stream.StreamResult\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.utils.ImageUtils\n * @author: Tony Shen\n * @date: 2025/2/21 18:16\n * @version: V1.0 <描述当前版本功能>\n */\n\n/**\n * 把 BufferedImage 转换成文件，便于调试时使用\n */\n@Throws(IOException::class)\nfun writeImageFile(bi: BufferedImage, fileName:String, formatName:String = \"png\"):Boolean {\n    return ImageIO.write(bi, formatName, File(fileName))\n}\n\n\nfun writeImageFileAsWebP(bi: BufferedImage, fileName:String):Boolean {\n\n    val writers = ImageIO.getImageWritersByFormatName(\"webp\")\n    if (!writers.hasNext()) {\n        println(\"不支持 WebP 格式，请确保 webp-imageio 插件已添加。\")\n        return false\n    }\n\n    val writer = writers.next()\n    val output = ImageIO.createImageOutputStream(File(fileName))\n    writer.output = output\n\n    writer.write(null, javax.imageio.IIOImage(bi, null, null), null)\n\n    output.close()\n    writer.dispose()\n    return true\n}\n\nfun loadAndFixSvg(inputSvgFile: File): Document {\n    val parser = XMLResourceDescriptor.getXMLParserClassName()\n    val factory = SAXSVGDocumentFactory(parser)\n    val doc = factory.createDocument(inputSvgFile.toURI().toString())\n\n    val svgNS = \"http://www.w3.org/2000/svg\"\n    val xlinkNS = \"http://www.w3.org/1999/xlink\"\n    val useTags = doc.getElementsByTagNameNS(svgNS, \"use\")\n\n    val toRemove = mutableListOf<Element>()\n    for (i in 0 until useTags.length) {\n        val use = useTags.item(i) as Element\n        val href = use.getAttributeNS(xlinkNS, \"href\")\n        if (href.isNullOrBlank()) {\n            toRemove.add(use)\n        }\n    }\n\n    toRemove.forEach { it.parentNode?.removeChild(it) }\n    println(\"清除非法 <use> 标签数: ${toRemove.size}\")\n    return doc\n}\n\nfun svgDocumentToBufferedImage(doc: Document, width: Float? = null, height: Float? = null): BufferedImage? {\n    var image: BufferedImage? = null\n\n    val transcoder = object : ImageTranscoder() {\n        override fun createImage(w: Int, h: Int): BufferedImage {\n            return BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB)\n        }\n\n        override fun writeImage(img: BufferedImage, output: TranscoderOutput?) {\n            image = img\n        }\n    }\n\n    if (width != null) transcoder.addTranscodingHint(ImageTranscoder.KEY_WIDTH, width)\n    if (height != null) transcoder.addTranscodingHint(ImageTranscoder.KEY_HEIGHT, height)\n\n    val inputStream = ByteArrayOutputStream().use { baos ->\n        val transformer = TransformerFactory.newInstance().newTransformer()\n        transformer.transform(DOMSource(doc), StreamResult(baos))\n        ByteArrayInputStream(baos.toByteArray())\n    }\n\n    val input = TranscoderInput(inputStream)\n\n    try {\n        transcoder.transcode(input, null)\n    } catch (e: Exception) {\n        e.printStackTrace()\n        return null\n    }\n\n    return image\n}\n\nfun loadFixedSvgAsImage(inputFile: File, width: Float? = null, height: Float? = null): BufferedImage? {\n    val doc = loadAndFixSvg(inputFile)\n    return svgDocumentToBufferedImage(doc, width, height)\n}\n\n/**\n * 在无需解码整张图片的情况下，获取图像的尺寸\n */\nfun getImageDimension(file: File): Pair<Int, Int>? {\n    ImageIO.createImageInputStream(file)?.use { input ->\n        val readers = ImageIO.getImageReaders(input)\n        if (readers.hasNext()) {\n            val reader = readers.next()\n            reader.input = input\n            val width = reader.getWidth(0)\n            val height = reader.getHeight(0)\n            reader.dispose()\n            return width to height\n        }\n    }\n    return null\n}\n\n/**\n * 判断图像是否大图\n */\nfun isLargeImage(width: Int, height: Int): Boolean {\n    val pixelCount = width * height\n    val longSide = maxOf(width, height)\n\n    return pixelCount > 12_000_000 || longSide > 4000 // 1200 万像素或长边超 4000px\n}\n\nfun clamp(c: Int): Int {\n    return if (c > 255) 255 else if (c < 0) 0 else c\n}\n\nfun clamp(x: Int, a: Int, b: Int): Int {\n    return if (x < a) a else if (x > b) b else x\n}\n\n/**\n * Clamp a value to an interval.\n * @param a the lower clamp threshold\n * @param b the upper clamp threshold\n * @param x the input parameter\n * @return the clamped value\n */\nfun clamp(x: Float, a: Float, b: Float): Float {\n    return if (x < a) a else if (x > b) b else x\n}\n\nfun premultiply(p: IntArray, offset: Int, length: Int) {\n    var length = length\n    length += offset\n    for (i in offset until length) {\n        val rgb = p[i]\n        val a = (rgb shr 24) and 0xff\n        var r = (rgb shr 16) and 0xff\n        var g = (rgb shr 8) and 0xff\n        var b = rgb and 0xff\n        val f = a * (1.0f / 255.0f)\n        r = (r * f).toInt()\n        g = (g * f).toInt()\n        b = (b * f).toInt()\n        p[i] = (a shl 24) or (r shl 16) or (g shl 8) or b\n    }\n}\n\nfun unpremultiply(p: IntArray, offset: Int, length: Int) {\n    var length = length\n    length += offset\n    for (i in offset until length) {\n        val rgb = p[i]\n        val a = (rgb shr 24) and 0xff\n        var r = (rgb shr 16) and 0xff\n        var g = (rgb shr 8) and 0xff\n        var b = rgb and 0xff\n        if (a != 0 && a != 255) {\n            val f = 255.0f / a\n            r = (r * f).toInt()\n            g = (g * f).toInt()\n            b = (b * f).toInt()\n            if (r > 255) r = 255\n            if (g > 255) g = 255\n            if (b > 255) b = 255\n            p[i] = (a shl 24) or (r shl 16) or (g shl 8) or b\n        }\n    }\n}"
  },
  {
    "path": "imageprocess/src/main/kotlin/cn/netdiscovery/monica/imageprocess/utils/extension/BufferedImage+Extensions.kt",
    "content": "package cn.netdiscovery.monica.imageprocess.utils.extension\n\nimport cn.netdiscovery.monica.imageprocess.ImageInfo\nimport java.awt.Color\nimport java.awt.Image\nimport java.awt.RenderingHints\nimport java.awt.geom.AffineTransform\nimport java.awt.image.BufferedImage\nimport java.io.ByteArrayOutputStream\nimport javax.imageio.ImageIO\nimport kotlin.math.abs\nimport kotlin.math.cos\nimport kotlin.math.floor\nimport kotlin.math.sin\n\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.utils.extension.`BufferedImage+Extension`\n * @author: Tony Shen\n * @date: 2025/2/22 15:00\n * @version: V1.0 <描述当前版本功能>\n */\nfun BufferedImage.image2ByteArray() : ByteArray {\n    val outStream = ByteArrayOutputStream()\n    ImageIO.write(this, \"png\", outStream)\n    return outStream.toByteArray()\n}\n\nfun BufferedImage.toImageInfo(): ImageInfo {\n\n    val width = this.width\n    val height = this.height\n    val byteArray = this.image2ByteArray()\n\n    return ImageInfo(width,height,byteArray)\n}\n\n/**\n * 对两个图像按像素进行比较\n */\nfun BufferedImage.isEqualTo(image: BufferedImage): Boolean {\n    if (width != image.width || height != image.height)\n        return false\n\n    for (y in 0 until height) {\n        for (x in 0 until width) {\n            if (getRGB(x, y) != image.getRGB(x, y))\n                return false\n        }\n    }\n\n    return true\n}\n\n\n/**\n * 对图像裁剪 ROI 区域\n */\nfun BufferedImage.subImage(x: Int, y: Int, w: Int, h: Int): BufferedImage {\n    if (w < 0 || h < 0)\n        throw IllegalArgumentException(\"Width and height should be non-negative: ($w; $h)\")\n\n    var x1 = x\n    var x2 = x + w     // w >= 0 => x1 <= x2\n    x1 = x1.coerceIn(0, width)\n    x2 = x2.coerceIn(0, width)\n\n    var y1 = y\n    var y2 = y + h     // h >= 0 => y1 <= y2\n    y1 = y1.coerceIn(0, height)\n    y2 = y2.coerceIn(0, height)\n\n    if (x2 - x1 == 0 || y2 - y1 == 0)\n        return BufferedImage(1, 1, this.type)\n\n    return getSubimage(x1, y1, x2 - x1, y2 - y1)\n}\n\n/**\n * 对图像水平翻转\n */\nfun BufferedImage.flipHorizontally(): BufferedImage {\n    val flipped = BufferedImage(width, height, type)\n    val tran = AffineTransform.getTranslateInstance(width.toDouble(), 0.0)\n    val flip = AffineTransform.getScaleInstance(-1.0, 1.0)\n\n    tran.concatenate(flip)\n\n    val g = flipped.createGraphics()\n    g.transform = tran\n    g.drawImage(this, 0, 0, null)\n    g.dispose()\n\n    return flipped\n}\n\n/**\n * 图像旋转\n */\nfun BufferedImage.rotate(angle: Double): BufferedImage {\n    val radian = Math.toRadians(angle)\n    val sin = abs(sin(radian))\n    val cos = abs(cos(radian))\n    val newWidth = floor(width.toDouble() * cos + height.toDouble() * sin).toInt()\n    val newHeight = floor(height.toDouble() * cos + width.toDouble() * sin).toInt()\n    val rotatedImage = BufferedImage(newWidth, newHeight, type)\n    val graphics = rotatedImage.createGraphics()\n    graphics.setRenderingHint(\n        RenderingHints.KEY_INTERPOLATION,\n        RenderingHints.VALUE_INTERPOLATION_BICUBIC\n    )\n    graphics.translate((newWidth - width) / 2, (newHeight - height) / 2)\n    // rotation around the center point\n    graphics.rotate(radian, (width / 2).toDouble(), (height / 2).toDouble())\n    graphics.drawImage(this, 0, 0, null)\n    graphics.dispose()\n    return rotatedImage\n}\n\n/**\n * 对图像进行缩放\n */\nfun BufferedImage.resize(width:Int, height:Int): BufferedImage {\n\n    val tmp = this.getScaledInstance(width, height, Image.SCALE_SMOOTH)\n    val resizedImage = BufferedImage(width, height, type)\n    val g2d = resizedImage.createGraphics()\n    try {\n        g2d.drawImage(tmp, 0, 0, null)\n    } finally {\n        g2d.dispose()\n    }\n    return resizedImage\n}\n\nfun BufferedImage.convertToRGB(): BufferedImage {\n    val rgbImage = BufferedImage(this.width, this.height, BufferedImage.TYPE_INT_RGB)\n    val g = rgbImage.createGraphics()\n    g.drawImage(this, 0, 0, Color.WHITE, null) // 用白色背景填充透明区域\n    g.dispose()\n    return rgbImage\n}"
  },
  {
    "path": "opencv/build.gradle.kts",
    "content": "plugins {\n    kotlin(\"jvm\")\n}\n\nrepositories {\n    mavenCentral()\n}\n\ndependencies {\n    testImplementation(kotlin(\"test\"))\n    implementation (\"org.jetbrains.kotlin:kotlin-stdlib\")\n    implementation(project(\":config\"))\n    implementation(project(\":domain\"))\n}\n\ntasks.test {\n    useJUnitPlatform()\n}\nkotlin {\n    jvmToolchain(17)\n}"
  },
  {
    "path": "opencv/src/main/kotlin/cn/netdiscovery/monica/opencv/ImageProcess.kt",
    "content": "package cn.netdiscovery.monica.opencv\n\nimport cn.netdiscovery.monica.config.isMac\nimport cn.netdiscovery.monica.config.isWindows\nimport cn.netdiscovery.monica.domain.*\nimport java.io.File\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.opencv.ImageProcess\n * @author: Tony Shen\n * @date:  2024/7/14 21:22\n * @version: V1.0 <描述当前版本功能>\n */\nobject ImageProcess {\n\n    private val loadPath by lazy{\n        System.getProperty(\"compose.application.resources.dir\") + File.separator\n    }\n\n    val resourcesDir by lazy {\n        File(loadPath)\n    }\n\n    init {\n        // 需要先加载图像处理库，否则无法通过 jni 调用算法\n        loadMonicaImageProcess()\n    }\n\n    /**\n     * 对于不同的平台加载的库是不同的: mac 是 dylib 库、windows 是 dll 库、linux 是 so 库\n     */\n    private fun loadMonicaImageProcess() {\n        if (isMac) {\n            System.load(\"${loadPath}libMonicaImageProcess.dylib\")\n        } else if (isWindows) {\n            System.load(\"${loadPath}libraw.dll\")\n            System.load(\"${loadPath}libde265.dll\")\n            System.load(\"${loadPath}aom.dll\")\n            System.load(\"${loadPath}heif.dll\")\n            System.load(\"${loadPath}opencv_world481.dll\")\n            System.load(\"${loadPath}MonicaImageProcess.dll\")\n        } else {\n            System.load(\"${loadPath}libMonicaImageProcess.so\")\n        }\n    }\n\n    /**\n     * 该算法库的版本号\n     */\n    external fun getVersion():String\n\n    /**\n     * 当前使用的 OpenCV 的版本号\n     */\n    external fun getOpenCVVersion():String\n\n    /**\n     * 图像错切\n     * @param 沿 x 方向\n     * @param 沿 y 方向\n     */\n    external fun shearing(src: ByteArray, x:Float, y:Float):IntArray\n\n    /**\n     * 初始化图像调色模块\n     */\n    external fun initColorCorrection(src: ByteArray): Long\n\n    /**\n     * 图像调色\n     */\n    external fun colorCorrection(src: ByteArray, colorCorrectionSettings: ColorCorrectionSettings, cppObjectPtr:Long):IntArray\n\n    /**\n     * 删除 ColorCorrection\n     */\n    external fun deleteColorCorrection(cppObjectPtr:Long): Long\n\n    /**\n     * 直方图均衡化\n     */\n    external fun equalizeHist(src: ByteArray):IntArray\n\n    /**\n     * 限制对比度自适应直方图均衡\n     */\n    external fun clahe(src: ByteArray, clipLimit:Double, size:Int):IntArray\n\n    /**\n     * gamma 校正\n     */\n    external fun gammaCorrection(src: ByteArray,k:Float):IntArray\n\n    /**\n     * laplace 锐化，主要是 8 邻域卷积核\n     */\n    external fun laplaceSharpening(src: ByteArray):IntArray\n\n    /**\n     * USM 锐化\n     */\n    external fun unsharpMask(src: ByteArray, radius:Int, threshold:Int, amount:Int):IntArray\n\n    /**\n     * 自动色彩均衡\n     */\n    external fun ace(src: ByteArray, ratio:Int, radius:Int):IntArray\n\n    /**\n     * 转换成灰度图像\n     */\n    external fun cvtGray(src: ByteArray):IntArray\n\n    /**\n     * 阈值分割\n     */\n    external fun threshold(src: ByteArray, thresholdType1: Int, thresholdType2: Int):IntArray\n\n    /**\n     * 自适应阈值分割\n     */\n    external fun adaptiveThreshold(src: ByteArray, adaptiveMethod: Int, thresholdType: Int, blockSize:Int, c:Int):IntArray\n\n    /**\n     * 颜色分割\n     */\n    external fun inRange(src: ByteArray, hmin:Int, smin:Int, vmin:Int, hmax:Int, smax:Int, vmax:Int):IntArray\n\n    /**\n     * 实现 roberts 算子\n     */\n    external fun roberts(src: ByteArray):IntArray\n\n    /**\n     * 实现 prewitt 算子\n     */\n    external fun prewitt(src: ByteArray):IntArray\n\n    /**\n     * 实现 sobel 算子\n     */\n    external fun sobel(src: ByteArray):IntArray\n\n    /**\n     * 实现 laplace 算子\n     */\n    external fun laplace(src: ByteArray):IntArray\n\n    /**\n     * 实现 canny 算子\n     */\n    external fun canny(src: ByteArray, threshold1:Double, threshold2: Double, apertureSize:Int):IntArray\n\n    /**\n     * 实现 LoG 算子\n     */\n    external fun log(src: ByteArray):IntArray\n\n    /**\n     * 实现 DoG 算子\n     */\n    external fun dog(src: ByteArray, sigma1:Double, sigma2:Double, size:Int):IntArray\n\n    /**\n     * 轮廓分析\n     */\n    external fun contourAnalysis(src: ByteArray, binary: ByteArray, scalar:IntArray, contourFilterSettings: ContourFilterSettings, contourDisplaySettings: ContourDisplaySettings):IntArray\n\n    /**\n     * 实现高斯滤波\n     */\n    external fun gaussianBlur(src: ByteArray, ksize:Int, sigmaX: Double = 0.0, sigmaY: Double = 0.0):IntArray\n\n    /**\n     * 实现中值滤波\n     */\n    external fun medianBlur(src: ByteArray, ksize:Int):IntArray\n\n    /**\n     * 实现高斯双边滤波\n     */\n    external fun bilateralFilter(src: ByteArray, d:Int, sigmaColor:Double, sigmaSpace:Double):IntArray\n\n    /**\n     * 实现均值迁移滤波\n     */\n    external fun pyrMeanShiftFiltering(src: ByteArray, sp: Double, sr: Double):IntArray\n\n    /**\n     * 形态学操作\n     */\n    external fun morphologyEx(src: ByteArray, morphologicalOperationSettings: MorphologicalOperationSettings):IntArray\n\n    /**\n     * 模版匹配\n     */\n    external fun matchTemplate(src: ByteArray, template: ByteArray, scalar:IntArray, matchTemplateSettings: MatchTemplateSettings):IntArray\n\n    /**\n     * 解码相机拍摄的图片(例如 cr2、cr3 格式的图像) 用于图像\n     */\n    external fun decodeRawToBufferForPreView(path: String): DecodedPreviewImage?\n\n    /**\n     * 解码相机拍摄的图片，并进行调色，调色完之后更新 PyramidImage 然后给 Kotlin 层返回 DecodedPreviewImage 对象。\n     */\n    external fun decodeRawAndColorCorrection(path: String, nativePtr:Long, colorCorrectionSettings: ColorCorrectionSettings, cppObjectPtr:Long): DecodedPreviewImage?\n\n    /**\n     * 针对 raw 文件、heic 文件\n     * 使用 PyramidImage 中的原图数据进行调色，调色完之后更新 PyramidImage 然后给 Kotlin 层返回 DecodedPreviewImage 对象。\n     * 所有的操作都在 C++ 层实现，确保速度。\n     */\n    external fun colorCorrectionWithPyramidImage(nativePtr:Long, colorCorrectionSettings: ColorCorrectionSettings, cppObjectPtr:Long): DecodedPreviewImage?\n\n    /**\n     * 解码大图（主要针对 jpg、png、webp 格式的大图），并返回 DecodedPreviewImage 对象\n     */\n    external fun decodeLargeImageToBufferForPreView(path: String): DecodedPreviewImage?\n\n    /**\n     * raw 文件、heic 文件，保存的时候通过 jni 获取原图的信息，然后进行保存。\n     * 该函数通过指针获取 Native 的 PyramidImage 对象，再返回原图的信息。\n     */\n    external fun getNativeImage(nativePtr:Long): NativeImage?\n\n    /**\n     * 删除 PyramidImage 对象\n     */\n    external fun deletePyramidImage(nativePtr:Long): Long\n\n    /**\n     * 解码 heif 格式的图像\n     */\n    external fun decodeHeif(path: String): DecodedPreviewImage?\n}"
  },
  {
    "path": "resources/common/filterConfig.json",
    "content": "[\n  {\n    \"name\": \"AverageFilter\",\n    \"desc\": \"均值模糊滤镜 - 通过平均邻域像素值实现平滑效果，有效减少图像噪声\",\n    \"remark\": \"适用于图像降噪和预处理，效果温和自然\",\n    \"params\": []\n  },\n  {\n    \"name\": \"BilateralFilter\",\n    \"desc\": \"双边滤波滤镜 - 在保持边缘清晰的同时平滑图像，智能降噪不损失细节\",\n    \"remark\": \"ds: 空间距离参数，rs: 颜色相似度参数。值越大效果越强\",\n    \"params\": [\n      { \"key\": \"ds\", \"type\": \"Double\", \"value\": 1.0 },\n      { \"key\": \"rs\", \"type\": \"Double\", \"value\": 1.0 }\n    ]\n  },\n  {\n    \"name\": \"BlockFilter\",\n    \"desc\": \"像素化滤镜 - 将图像分割成规则方块，创造复古像素艺术效果\",\n    \"remark\": \"blockSize: 方块大小，值越大像素化效果越明显\",\n    \"params\": [\n      { \"key\": \"blockSize\", \"type\": \"Int\", \"value\": 2 }\n    ]\n  },\n  {\n    \"name\": \"BoxBlurFilter\",\n    \"desc\": \"盒式模糊滤镜 - 使用矩形核进行快速模糊处理，适合大面积平滑\",\n    \"remark\": \"hRadius: 水平模糊半径，vRadius: 垂直模糊半径，iterations: 迭代次数\",\n    \"params\": [\n      { \"key\": \"hRadius\", \"type\": \"Int\", \"value\": 5 },\n      { \"key\": \"vRadius\", \"type\": \"Int\", \"value\": 5 },\n      { \"key\": \"iterations\", \"type\": \"Int\", \"value\": 1 }\n    ]\n  },\n  {\n    \"name\": \"BumpFilter\",\n    \"desc\": \"凹凸浮雕滤镜 - 通过边缘检测创造立体凹凸效果，增强纹理质感\",\n    \"remark\": \"适合为平面图像添加立体感和深度\",\n    \"params\": []\n  },\n  {\n    \"name\": \"CarveFilter\",\n    \"desc\": \"雕刻滤镜 - 模拟雕刻工艺效果，创造凹陷的立体视觉\",\n    \"remark\": \"与浮雕效果相反，产生向内凹陷的视觉效果\",\n    \"params\": []\n  },\n  {\n    \"name\": \"ColorFilter\",\n    \"desc\": \"色彩映射滤镜 - 应用预定义色彩方案，快速改变图像整体色调\",\n    \"remark\": \"支持12种经典色彩风格：0:秋色,1:骨骼,2:冷色,3:暖色,4:HSV,5:喷射,6:海洋,7:粉色,8:彩虹,9:春天,10:夏天,11:冬天\",\n    \"params\": [\n      { \"key\": \"style\", \"type\": \"Int\", \"value\": 0 }\n    ]\n  },\n  {\n    \"name\": \"ColorHalftoneFilter\",\n    \"desc\": \"彩色半调滤镜 - 模拟传统印刷网点效果，创造复古印刷风格\",\n    \"remark\": \"dotRadius: 网点半径，控制半调效果的精细程度\",\n    \"params\": [\n      { \"key\": \"dotRadius\", \"type\": \"Float\", \"value\":2.0 }\n    ]\n  },\n  {\n    \"name\": \"ConBriFilter\",\n    \"desc\": \"对比度亮度调节滤镜 - 精确控制图像明暗对比，提升视觉效果\",\n    \"remark\": \"brightness: 亮度调节(1.0为原始亮度)，contrast: 对比度调节(1.0为原始对比度)\",\n    \"params\": [\n      { \"key\": \"brightness\", \"type\": \"Float\", \"value\": 1.0 },\n      { \"key\": \"contrast\", \"type\": \"Float\", \"value\": 1.5 }\n    ]\n  },\n  {\n    \"name\": \"CrystallizeFilter\",\n    \"desc\": \"水晶化滤镜 - 将图像分解为不规则水晶块，创造抽象艺术效果\",\n    \"remark\": \"支持5种网格类型：0:随机,1:方形,2:六边形,3:八边形,4:三角形。scale控制水晶块大小\",\n    \"params\": [\n      { \"key\": \"edgeThickness\", \"type\": \"Float\", \"value\": 0.4 },\n      { \"key\": \"scale\", \"type\": \"Float\", \"value\": 16 },\n      { \"key\": \"randomness\", \"type\": \"Float\", \"value\": 0.0 },\n      { \"key\": \"gridType\", \"type\": \"Int\", \"value\": 2 }\n    ]\n  },\n  {\n    \"name\": \"CropFilter\",\n    \"desc\": \"裁剪滤镜 - 提取图像指定区域，用于局部处理或构图调整\",\n    \"remark\": \"x,y: 起始坐标，w,h: 裁剪区域宽高\",\n    \"params\": [\n      { \"key\": \"x\", \"type\": \"Int\", \"value\": 0 },\n      { \"key\": \"y\", \"type\": \"Int\", \"value\": 0 },\n      { \"key\": \"w\", \"type\": \"Int\", \"value\": 32 },\n      { \"key\": \"h\", \"type\": \"Int\", \"value\": 32 }\n    ]\n  },\n  {\n    \"name\": \"DiffuseFilter\",\n    \"desc\": \"扩散滤镜 - 模拟光线散射效果，创造柔和朦胧的视觉氛围\",\n    \"remark\": \"scale: 扩散强度，值越大效果越明显\",\n    \"params\": [\n      { \"key\": \"scale\", \"type\": \"Float\", \"value\": 4.0 }\n    ]\n  },\n  {\n    \"name\": \"EmbossFilter\",\n    \"desc\": \"浮雕滤镜 - 通过边缘高光创造立体浮雕效果，增强图像层次感\",\n    \"remark\": \"colorConstant: 浮雕强度，控制立体效果的明显程度\",\n    \"params\": [\n      { \"key\": \"colorConstant\", \"type\": \"Int\", \"value\": 100 }\n    ]\n  },\n  {\n    \"name\": \"EqualizeFilter\",\n    \"desc\": \"直方图均衡化滤镜 - 自动调整图像亮度分布，改善整体视觉效果\",\n    \"remark\": \"适用于曝光不足或对比度较低的图像，能显著提升图像质量\",\n    \"params\": []\n  },\n  {\n    \"name\": \"ExposureFilter\",\n    \"desc\": \"曝光调节滤镜 - 模拟相机曝光控制，调整图像整体明暗\",\n    \"remark\": \"exposure > 0，值越大图像越亮，适合修正曝光问题\",\n    \"params\": [\n      { \"key\": \"exposure\", \"type\": \"Float\", \"value\": 1 }\n    ]\n  },\n  {\n    \"name\": \"FastBlur2D\",\n    \"desc\": \"快速二维模糊滤镜 - 高效的模糊算法，适合实时处理\",\n    \"remark\": \"ksize: 模糊核大小，值越大模糊效果越强\",\n    \"params\": [\n      { \"key\": \"ksize\", \"type\": \"Int\", \"value\": 5 }\n    ]\n  },\n  {\n    \"name\": \"GainFilter\",\n    \"desc\": \"增益偏置滤镜 - 精确控制图像亮度和对比度，专业级调色工具\",\n    \"remark\": \"gain: 增益系数(0-1)，bias: 偏置值(0-1)，用于精细的亮度调节\",\n    \"params\": [\n      { \"key\": \"gain\", \"type\": \"Float\", \"value\": 0.5 },\n      { \"key\": \"bias\", \"type\": \"Float\", \"value\": 0.5 }\n    ]\n  },\n  {\n    \"name\": \"GammaFilter\",\n    \"desc\": \"伽马校正滤镜 - 调整图像中间调亮度，改善显示效果\",\n    \"remark\": \"gamma < 1图像变亮，gamma > 1图像变暗，常用于显示设备校正\",\n    \"params\": [\n      { \"key\": \"gamma\", \"type\": \"Double\", \"value\": 0.5 }\n    ]\n  },\n  {\n    \"name\": \"GaussianFilter\",\n    \"desc\": \"高斯模糊滤镜 - 经典的高斯分布模糊，效果自然平滑\",\n    \"remark\": \"radius: 模糊半径，基于高斯分布的最优模糊算法\",\n    \"params\": [\n      { \"key\": \"radius\", \"type\": \"Float\", \"value\": 5.0 }\n    ]\n  },\n  {\n    \"name\": \"GaussianNoiseFilter\",\n    \"desc\": \"高斯噪声滤镜 - 添加随机噪声，模拟胶片颗粒或数字噪点\",\n    \"remark\": \"sigma: 噪声强度，用于创造复古胶片效果或艺术化处理\",\n    \"params\": [\n      { \"key\": \"sigma\", \"type\": \"Int\", \"value\": 25 }\n    ]\n  },\n  {\n    \"name\": \"GradientFilter\",\n    \"desc\": \"梯度滤镜 - 使用Sobel算子检测边缘，突出图像轮廓\",\n    \"remark\": \"常用于边缘检测和图像分析，创造素描般的线条效果\",\n    \"params\": []\n  },\n  {\n    \"name\": \"GrayFilter\",\n    \"desc\": \"灰度滤镜 - 将彩色图像转换为灰度，保留亮度信息\",\n    \"remark\": \"经典的黑白转换，适合创造怀旧或艺术效果\",\n    \"params\": []\n  },\n  {\n    \"name\": \"HighPassFilter\",\n    \"desc\": \"高通滤波滤镜 - 突出图像高频细节，创造发光边缘效果\",\n    \"remark\": \"radius: 滤波半径，用于增强图像细节和创造特殊视觉效果\",\n    \"params\": [\n      { \"key\": \"radius\", \"type\": \"Float\", \"value\": 10.0 }\n    ]\n  },\n  {\n    \"name\": \"HSBAdjustFilter\",\n    \"desc\": \"HSB色彩调节滤镜 - 分别调节色相、饱和度、亮度\",\n    \"remark\": \"hFactor: 色相调节，sFactor: 饱和度调节，bFactor: 亮度调节\",\n    \"params\": [\n      { \"key\": \"hFactor\", \"type\": \"Float\", \"value\": 0.0 },\n      { \"key\": \"sFactor\", \"type\": \"Float\", \"value\": 0.0 },\n      { \"key\": \"bFactor\", \"type\": \"Float\", \"value\": 0.0 }\n    ]\n  },\n  {\n    \"name\": \"InvertFilter\",\n    \"desc\": \"反色滤镜 - 反转图像所有颜色，创造负片效果\",\n    \"remark\": \"经典的反色处理，常用于创造戏剧性视觉效果\",\n    \"params\": []\n  },\n  {\n    \"name\": \"LaplaceSharpenFilter\",\n    \"desc\": \"拉普拉斯锐化滤镜 - 使用拉普拉斯算子增强图像边缘和细节\",\n    \"remark\": \"专业的锐化算法，能有效提升图像清晰度\",\n    \"params\": []\n  },\n  {\n    \"name\": \"LensBlurFilter\",\n    \"desc\": \"镜头模糊滤镜 - 模拟真实镜头景深效果，创造专业摄影质感\",\n    \"remark\": \"radius: 模糊半径，bloom: 光晕强度，sides: 光圈边数(模拟不同镜头)\",\n    \"params\": [\n      { \"key\": \"radius\", \"type\": \"Float\", \"value\": 10.0 },\n      { \"key\": \"bloom\", \"type\": \"Float\", \"value\": 2.0 },\n      { \"key\": \"bloomThreshold\", \"type\": \"Float\", \"value\": 255.0 },\n      { \"key\": \"angle\", \"type\": \"Float\", \"value\": 0.0 },\n      { \"key\": \"sides\", \"type\": \"Int\", \"value\": 5 }\n    ]\n  },\n  {\n    \"name\": \"MarbleFilter\",\n    \"desc\": \"大理石纹理滤镜 - 模拟天然大理石纹理，创造优雅的材质效果\",\n    \"remark\": \"xScale/yScale: 纹理缩放，turbulence: 湍流强度，控制纹理复杂度\",\n    \"params\": [\n      { \"key\": \"xScale\", \"type\": \"Float\", \"value\": 4.0 },\n      { \"key\": \"yScale\", \"type\": \"Float\", \"value\": 4.0 },\n      { \"key\": \"turbulence\", \"type\": \"Float\", \"value\": 1.0 }\n    ]\n  },\n  {\n    \"name\": \"MaximumFilter\",\n    \"desc\": \"最大值滤波滤镜 - 用邻域最大值替换像素，创造膨胀效果\",\n    \"remark\": \"形态学处理，用于图像分析和特殊效果处理\",\n    \"params\": []\n  },\n  {\n    \"name\": \"MinimumFilter\",\n    \"desc\": \"最小值滤波滤镜 - 用邻域最小值替换像素，创造腐蚀效果\",\n    \"remark\": \"形态学处理，与最大值滤波配合使用\",\n    \"params\": []\n  },\n  {\n    \"name\": \"MirrorFilter\",\n    \"desc\": \"镜像滤镜 - 创建图像镜像效果，适合对称构图\",\n    \"remark\": \"opacity: 镜像透明度，centreY: 镜像中心，gap: 镜像间隙\",\n    \"params\": [\n      { \"key\": \"opacity\", \"type\": \"Float\", \"value\": 1.0 },\n      { \"key\": \"centreY\", \"type\": \"Float\", \"value\": 0.5 },\n      { \"key\": \"gap\", \"type\": \"Float\", \"value\": 0.0 }\n    ]\n  },\n  {\n    \"name\": \"MosaicFilter\",\n    \"desc\": \"马赛克滤镜 - 将图像分割为规则色块，创造抽象艺术效果\",\n    \"remark\": \"r: 马赛克块大小，值越大马赛克效果越明显\",\n    \"params\": [\n      { \"key\": \"r\", \"type\": \"Int\", \"value\": 3 }\n    ]\n  },\n  {\n    \"name\": \"MotionFilter\",\n    \"desc\": \"运动模糊滤镜 - 模拟物体运动轨迹，创造动态视觉效果\",\n    \"remark\": \"angle: 运动方向，distance: 模糊距离，zoom: 缩放效果\",\n    \"params\": [\n      { \"key\": \"angle\", \"type\": \"Float\", \"value\": 0.0 },\n      { \"key\": \"distance\", \"type\": \"Float\", \"value\": 0.0 },\n      { \"key\": \"zoom\", \"type\": \"Float\", \"value\": 0.4 }\n    ]\n  },\n  {\n    \"name\": \"NatureFilter\",\n    \"desc\": \"自然风格滤镜 - 模拟自然现象的色彩效果，创造独特的艺术风格\",\n    \"remark\": \"支持8种自然风格：1:大气,2:燃烧,3:雾霾,4:冰冻,5:熔岩,6:金属,7:海洋,8:水流\",\n    \"params\": [\n      { \"key\": \"style\", \"type\": \"Int\", \"value\": 1 }\n    ]\n  },\n  {\n    \"name\": \"OffsetFilter\",\n    \"desc\": \"偏移滤镜 - 移动图像位置，用于特殊构图或效果组合\",\n    \"remark\": \"xOffset: 水平偏移，yOffset: 垂直偏移，支持负值\",\n    \"params\": [\n      { \"key\": \"xOffset\", \"type\": \"Int\", \"value\": 0 },\n      { \"key\": \"yOffset\", \"type\": \"Int\", \"value\": 0 }\n    ]\n  },\n  {\n    \"name\": \"OilPaintFilter\",\n    \"desc\": \"油画滤镜 - 模拟油画笔触效果，创造艺术绘画质感\",\n    \"remark\": \"intensity: 油画强度，ksize: 笔触大小，控制油画效果的细腻程度\",\n    \"params\": [\n      { \"key\": \"intensity\", \"type\": \"Int\", \"value\": 40 },\n      { \"key\": \"ksize\", \"type\": \"Int\", \"value\": 10 }\n    ]\n  },\n  {\n    \"name\": \"PointillizeFilter\",\n    \"desc\": \"点彩滤镜 - 将图像转换为彩色点阵，模拟点彩画派艺术风格\",\n    \"remark\": \"支持5种点阵类型：0:随机,1:方形,2:六边形,3:八边形,4:三角形。scale控制点的大小\",\n    \"params\": [\n      { \"key\": \"edgeThickness\", \"type\": \"Float\", \"value\": 0.4 },\n      { \"key\": \"fuzziness\", \"type\": \"Float\", \"value\": 0.1 },\n      { \"key\": \"scale\", \"type\": \"Float\", \"value\": 16 },\n      { \"key\": \"randomness\", \"type\": \"Float\", \"value\": 0.0 },\n      { \"key\": \"gridType\", \"type\": \"Int\", \"value\": 2 }\n    ]\n  },\n  {\n    \"name\": \"PosterizeFilter\",\n    \"desc\": \"色调分离滤镜 - 减少颜色层次，创造海报化的艺术效果\",\n    \"remark\": \"numLevels: 颜色层次数，值越小效果越明显，创造强烈的视觉冲击\",\n    \"params\": [\n      { \"key\": \"numLevels\", \"type\": \"Int\", \"value\": 6 }\n    ]\n  },\n  {\n    \"name\": \"RippleFilter\",\n    \"desc\": \"波纹滤镜 - 创造水面波纹效果，模拟液体表面的波动\",\n    \"remark\": \"支持4种波形：0:正弦波,1:锯齿波,2:三角波,3:噪声波。xAmplitude/yAmplitude控制波纹幅度\",\n    \"params\": [\n      { \"key\": \"xAmplitude\", \"type\": \"Float\", \"value\": 5.0 },\n      { \"key\": \"yAmplitude\", \"type\": \"Float\", \"value\": 0.0 },\n      { \"key\": \"xWavelength\", \"type\": \"Float\", \"value\": 16.0 },\n      { \"key\": \"yWavelength\", \"type\": \"Float\", \"value\": 16.0 },\n      { \"key\": \"waveType\", \"type\": \"Int\", \"value\": 0 }\n    ]\n  },\n  {\n    \"name\": \"SepiaToneFilter\",\n    \"desc\": \"棕褐色滤镜 - 创造怀旧老照片效果，营造复古氛围\",\n    \"remark\": \"经典的复古色调，常用于创造历史感和怀旧情绪\",\n    \"params\": []\n  },\n  {\n    \"name\": \"SharpenFilter\",\n    \"desc\": \"锐化滤镜 - 增强图像边缘和细节，提升图像清晰度\",\n    \"remark\": \"基础的锐化处理，适用于轻微模糊的图像\",\n    \"params\": []\n  },\n  {\n    \"name\": \"SmearFilter\",\n    \"desc\": \"涂抹滤镜 - 模拟绘画涂抹效果，创造艺术化的笔触质感\",\n    \"remark\": \"支持4种涂抹形状：0:十字,1:线条,2:圆形,3:方形。density控制涂抹密度\",\n    \"params\": [\n      { \"key\": \"angle\", \"type\": \"Float\", \"value\": 0.0 },\n      { \"key\": \"density\", \"type\": \"Float\", \"value\": 0.5 },\n      { \"key\": \"distance\", \"type\": \"Int\", \"value\": 8 },\n      { \"key\": \"shape\", \"type\": \"Int\", \"value\": 1 },\n      { \"key\": \"mix\", \"type\": \"Float\", \"value\": 0.5 }\n    ]\n  },\n  {\n    \"name\": \"SolarizeFilter\",\n    \"desc\": \"日晒滤镜 - 模拟过度曝光效果，创造高对比度的艺术效果\",\n    \"remark\": \"经典的摄影特效，创造戏剧性的视觉冲击\",\n    \"params\": []\n  },\n  {\n    \"name\": \"SpotlightFilter\",\n    \"desc\": \"聚光灯滤镜 - 模拟聚光灯照射效果，创造局部高亮\",\n    \"remark\": \"factor: 聚光强度，用于突出图像特定区域\",\n    \"params\": [\n      { \"key\": \"factor\", \"type\": \"Int\", \"value\": 1 }\n    ]\n  },\n  {\n    \"name\": \"StrokeAreaFilter\",\n    \"desc\": \"描边滤镜 - 提取图像边缘轮廓，创造铅笔画效果\",\n    \"remark\": \"ksize: 描边粗细，控制线条的粗细程度\",\n    \"params\": [\n      { \"key\": \"ksize\", \"type\": \"Double\", \"value\": 10.0 }\n    ]\n  },\n  {\n    \"name\": \"SwimFilter\",\n    \"desc\": \"水下滤镜 - 模拟水下视觉效果，创造扭曲的液体环境\",\n    \"remark\": \"scale: 扭曲强度，amount: 效果强度，turbulence: 湍流程度\",\n    \"params\": [\n      { \"key\": \"scale\", \"type\": \"Float\", \"value\": 32.0 },\n      { \"key\": \"stretch\", \"type\": \"Float\", \"value\": 1.0 },\n      { \"key\": \"angle\", \"type\": \"Float\", \"value\": 0.0 },\n      { \"key\": \"amount\", \"type\": \"Float\", \"value\": 1.0 },\n      { \"key\": \"turbulence\", \"type\": \"Float\", \"value\": 1.0 },\n      { \"key\": \"time\", \"type\": \"Float\", \"value\": 0.0 }\n    ]\n  },\n  {\n    \"name\": \"USMFilter\",\n    \"desc\": \"USM锐化滤镜 - 专业级锐化算法，在保持自然度的同时增强细节\",\n    \"remark\": \"radius: 锐化半径，amount: 锐化强度，threshold: 锐化阈值。专业摄影师常用\",\n    \"params\": [\n      { \"key\": \"radius\", \"type\": \"Float\", \"value\": 2.0 },\n      { \"key\": \"amount\", \"type\": \"Float\", \"value\": 0.5 },\n      { \"key\": \"threshold\", \"type\": \"Int\", \"value\": 1 }\n    ]\n  },\n  {\n    \"name\": \"VariableBlurFilter\",\n    \"desc\": \"可变模糊滤镜 - 使用不同模糊半径，创造渐变模糊效果\",\n    \"remark\": \"hRadius: 水平模糊半径，vRadius: 垂直模糊半径，iterations: 迭代次数\",\n    \"params\": [\n      { \"key\": \"hRadius\", \"type\": \"Int\", \"value\": 5 },\n      { \"key\": \"vRadius\", \"type\": \"Int\", \"value\": 5 },\n      { \"key\": \"iterations\", \"type\": \"Int\", \"value\": 1 }\n    ]\n  },\n  {\n    \"name\": \"VignetteFilter\",\n    \"desc\": \"暗角滤镜 - 创造边缘渐暗效果，突出中心主体，增强视觉焦点\",\n    \"remark\": \"fade: 暗角强度，vignetteWidth: 暗角宽度，经典的照片后期处理效果\",\n    \"params\": [\n      { \"key\": \"fade\", \"type\": \"Int\", \"value\": 35 },\n      { \"key\": \"vignetteWidth\", \"type\": \"Int\", \"value\": 50 }\n    ]\n  },\n  {\n    \"name\": \"WaterFilter\",\n    \"desc\": \"水波纹滤镜 - 创造逼真的水面波纹效果，模拟液体波动\",\n    \"remark\": \"wavelength: 波长，amplitude: 振幅，centreX/centreY: 波纹中心，radius: 影响半径\",\n    \"params\": [\n      { \"key\": \"wavelength\", \"type\": \"Float\", \"value\": 16.0 },\n      { \"key\": \"amplitude\", \"type\": \"Float\", \"value\": 10.0 },\n      { \"key\": \"phase\", \"type\": \"Float\", \"value\": 0.0 },\n      { \"key\": \"centreX\", \"type\": \"Float\", \"value\": 0.5 },\n      { \"key\": \"centreY\", \"type\": \"Float\", \"value\": 0.5 },\n      { \"key\": \"radius\", \"type\": \"Float\", \"value\": 50.0 }\n    ]\n  },\n  {\n    \"name\": \"WhiteImageFilter\",\n    \"desc\": \"增白滤镜 - 提升图像整体亮度，创造清新明亮的视觉效果\",\n    \"remark\": \"beta: 增白系数，值越大图像越亮，适合修正曝光不足\",\n    \"params\": [\n      { \"key\": \"beta\", \"type\": \"Double\", \"value\": 1.1 }\n    ]\n  }\n]"
  },
  {
    "path": "resources/package.json",
    "content": "{\n  \"name\": \"monica-web-screenshot\",\n  \"version\": \"1.0.0\",\n  \"description\": \"Web screenshot service for Monica\",\n  \"main\": \"web-screenshot.js\",\n  \"scripts\": {\n    \"screenshot\": \"node web-screenshot.js\"\n  },\n  \"dependencies\": {\n    \"playwright\": \"^1.40.0\"\n  }\n}\n"
  },
  {
    "path": "resources/web-screenshot.js",
    "content": "const { chromium } = require('playwright');\nconst fs = require('fs');\nconst path = require('path');\n\n// 解析命令行参数\nconst args = process.argv.slice(2);\nconst url = args[0];\nconst outputPath = args[1];\n\nif (!url || !outputPath) {\n    console.error('用法: node web-screenshot.js <url> <outputPath> [options]');\n    process.exit(1);\n}\n\n// 解析选项\nconst options = {\n    fullPage: true,\n    waitUntil: 'networkidle',\n    timeout: 30000,\n    viewportWidth: null,\n    viewportHeight: null,\n    deviceScaleFactor: 2,\n    cookiesFile: null\n};\n\nargs.slice(2).forEach(arg => {\n    if (arg.startsWith('--fullPage=')) {\n        options.fullPage = arg.split('=')[1] === 'true';\n    } else if (arg.startsWith('--waitUntil=')) {\n        options.waitUntil = arg.split('=')[1];\n    } else if (arg.startsWith('--timeout=')) {\n        options.timeout = parseInt(arg.split('=')[1]);\n    } else if (arg.startsWith('--viewportWidth=')) {\n        options.viewportWidth = parseInt(arg.split('=')[1]);\n    } else if (arg.startsWith('--viewportHeight=')) {\n        options.viewportHeight = parseInt(arg.split('=')[1]);\n    } else if (arg.startsWith('--deviceScaleFactor=')) {\n        options.deviceScaleFactor = parseFloat(arg.split('=')[1]);\n    } else if (arg.startsWith('--cookiesFile=')) {\n        options.cookiesFile = arg.split('=')[1];\n    }\n});\n\nasync function autoScrollPage(page, timeout) {\n    await page.evaluate(async maxDuration => {\n        const delay = ms => new Promise(resolve => setTimeout(resolve, ms));\n        const startTime = Date.now();\n        let lastHeight = -1;\n        let stableRounds = 0;\n\n        while (Date.now() - startTime < maxDuration) {\n            const viewportHeight = window.innerHeight || 900;\n            const currentHeight = Math.max(\n                document.body.scrollHeight,\n                document.documentElement.scrollHeight\n            );\n\n            window.scrollBy(0, Math.max(400, Math.floor(viewportHeight * 0.85)));\n            await delay(250);\n\n            const nextHeight = Math.max(\n                document.body.scrollHeight,\n                document.documentElement.scrollHeight\n            );\n\n            if (nextHeight === lastHeight && window.innerHeight + window.scrollY >= nextHeight - 4) {\n                stableRounds += 1;\n            } else {\n                stableRounds = 0;\n            }\n\n            lastHeight = nextHeight;\n\n            if (stableRounds >= 3) {\n                break;\n            }\n        }\n\n        window.scrollTo(0, 0);\n        await delay(200);\n    }, Math.min(timeout, 15000));\n}\n\nasync function forceLazyImages(page) {\n    await page.evaluate(() => {\n        const selectors = ['img', 'source', '[data-src]', '[data-original]', '[data-lazy-src]'];\n\n        document.querySelectorAll(selectors.join(',')).forEach(node => {\n            if (node.tagName === 'IMG') {\n                node.loading = 'eager';\n                node.decoding = 'sync';\n            }\n\n            const dataSrc = node.getAttribute('data-src');\n            const dataOriginal = node.getAttribute('data-original');\n            const dataLazySrc = node.getAttribute('data-lazy-src');\n\n            if (node.tagName === 'IMG' && !node.getAttribute('src')) {\n                const fallbackSrc = dataSrc || dataOriginal || dataLazySrc;\n                if (fallbackSrc) {\n                    node.setAttribute('src', fallbackSrc);\n                }\n            }\n\n            if (node.tagName === 'SOURCE' && !node.getAttribute('srcset')) {\n                const fallbackSrcSet = dataSrc || dataOriginal || dataLazySrc;\n                if (fallbackSrcSet) {\n                    node.setAttribute('srcset', fallbackSrcSet);\n                }\n            }\n        });\n    });\n}\n\nasync function waitForImages(page, timeout) {\n    await page.evaluate(async maxDuration => {\n        const delay = ms => new Promise(resolve => setTimeout(resolve, ms));\n        const startTime = Date.now();\n\n        while (Date.now() - startTime < maxDuration) {\n            const pendingImages = Array.from(document.images).filter(img => {\n                const style = window.getComputedStyle(img);\n                const visible = style.display !== 'none' && style.visibility !== 'hidden';\n                const hasSource = Boolean(img.currentSrc || img.src);\n                return visible && hasSource && !img.complete;\n            });\n\n            if (pendingImages.length === 0) {\n                await delay(300);\n\n                const recheckPending = Array.from(document.images).filter(img => {\n                    const style = window.getComputedStyle(img);\n                    const visible = style.display !== 'none' && style.visibility !== 'hidden';\n                    const hasSource = Boolean(img.currentSrc || img.src);\n                    return visible && hasSource && !img.complete;\n                });\n\n                if (recheckPending.length === 0) {\n                    break;\n                }\n            }\n\n            await delay(250);\n        }\n    }, Math.min(timeout, 10000));\n}\n\nasync function settleLongPage(page, timeout) {\n    await forceLazyImages(page);\n    await autoScrollPage(page, timeout);\n    await forceLazyImages(page);\n    await waitForImages(page, timeout);\n}\n\nasync function captureScreenshot() {\n    let browser = null;\n    try {\n        console.log(`开始截图: ${url}`);\n        \n        browser = await chromium.launch({\n            headless: true\n        });\n        \n        const page = await browser.newPage({\n            deviceScaleFactor: Number.isFinite(options.deviceScaleFactor) && options.deviceScaleFactor > 0\n                ? options.deviceScaleFactor\n                : 2\n        });\n        \n        // 设置视口\n        if (options.viewportWidth && options.viewportHeight) {\n            await page.setViewportSize({\n                width: options.viewportWidth,\n                height: options.viewportHeight\n            });\n        }\n        \n        // 加载 Cookie（如果提供）\n        if (options.cookiesFile && fs.existsSync(options.cookiesFile)) {\n            try {\n                const cookiesJson = fs.readFileSync(options.cookiesFile, 'utf8');\n                const cookies = JSON.parse(cookiesJson);\n                if (Array.isArray(cookies) && cookies.length > 0) {\n                    // 提取域名（从 URL）\n                    const urlObj = new URL(url);\n                    const domain = urlObj.hostname;\n                    \n                    // 设置 Cookie\n                    await page.context().addCookies(cookies.map(cookie => ({\n                        name: cookie.name,\n                        value: cookie.value,\n                        domain: cookie.domain || domain,\n                        path: cookie.path || '/',\n                        expires: cookie.expires || Math.floor(Date.now() / 1000) + 86400, // 默认1天后过期\n                        httpOnly: cookie.httpOnly || false,\n                        secure: cookie.secure || false,\n                        sameSite: cookie.sameSite || 'Lax'\n                    })));\n                    console.log(`已加载 ${cookies.length} 个 Cookie`);\n                }\n            } catch (error) {\n                throw new Error(`加载 Cookie 失败: ${error.message}`);\n            }\n        }\n        \n        // 导航到页面\n        await page.goto(url, {\n            waitUntil: options.waitUntil,\n            timeout: options.timeout\n        });\n        \n        // 等待页面完全加载（额外等待动态内容）\n        await page.waitForTimeout(1000);\n\n        if (options.fullPage) {\n            console.log('开始预加载长页内容');\n            await settleLongPage(page, options.timeout);\n        } else {\n            await forceLazyImages(page);\n            await waitForImages(page, options.timeout);\n        }\n        \n        // 截图\n        console.log(`截图选项: fullPage=${options.fullPage}, waitUntil=${options.waitUntil}`);\n        await page.screenshot({\n            path: outputPath,\n            fullPage: options.fullPage,\n            type: 'png'\n        });\n        \n        console.log(`截图成功保存到: ${outputPath}`);\n        return 0;\n        \n    } catch (error) {\n        console.error(`截图失败: ${error.message}`);\n        console.error(error.stack);\n        return 1;\n    } finally {\n        if (browser) {\n            await browser.close();\n        }\n    }\n}\n\ncaptureScreenshot()\n    .then(code => {\n        process.exitCode = code;\n    })\n    .catch(error => {\n        console.error(`截图异常: ${error.message}`);\n        console.error(error.stack);\n        process.exitCode = 1;\n    });\n"
  },
  {
    "path": "settings.gradle.kts",
    "content": "pluginManagement {\n    repositories {\n        google()\n        gradlePluginPortal()\n        mavenCentral()\n        maven(\"https://maven.pkg.jetbrains.space/public/p/compose/dev\")\n        maven( \"https://jitpack.io\" )\n    }\n\n    plugins {\n        kotlin(\"multiplatform\").version(extra[\"kotlin.version\"] as String)\n        id(\"org.jetbrains.compose\").version(extra[\"compose.version\"] as String)\n    }\n}\nplugins {\n    id(\"org.gradle.toolchains.foojay-resolver-convention\") version \"0.8.0\"\n}\n\nrootProject.name = \"Monica\"\ninclude(\"domain\")\ninclude(\"opencv\")\ninclude(\"config\")\ninclude(\"imageprocess\")\ninclude(\"i18n\")\n"
  },
  {
    "path": "src/jvmMain/kotlin/Main.kt",
    "content": "import androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.window.*\nimport cn.netdiscovery.monica.config.*\nimport cn.netdiscovery.monica.config.category.ConfigDefinitions\nimport cn.netdiscovery.monica.config.storage.ConfigManager\nimport cn.netdiscovery.monica.rxcache.rxCache\nimport cn.netdiscovery.monica.di.viewModelModule\nimport cn.netdiscovery.monica.history.EditHistoryCenter\nimport cn.netdiscovery.monica.http.healthCheck\nimport cn.netdiscovery.monica.i18n.LocalizationManager\nimport cn.netdiscovery.monica.rxcache.getFilterNames\nimport cn.netdiscovery.monica.rxcache.initFilterMap\nimport cn.netdiscovery.monica.rxcache.initFilterParamsConfig\nimport cn.netdiscovery.monica.state.*\nimport cn.netdiscovery.monica.ui.controlpanel.ai.experiment.experiment\nimport cn.netdiscovery.monica.ui.controlpanel.ai.faceswap.faceSwap\nimport cn.netdiscovery.monica.ui.controlpanel.cartoon.cartoon\nimport cn.netdiscovery.monica.ui.controlpanel.colorcorrection.colorCorrection\nimport cn.netdiscovery.monica.ui.controlpanel.colorpick.colorPick\nimport cn.netdiscovery.monica.ui.controlpanel.cropimage.cropImage\nimport cn.netdiscovery.monica.ui.controlpanel.doodle.drawImage\nimport cn.netdiscovery.monica.ui.controlpanel.filter.filter\nimport cn.netdiscovery.monica.ui.controlpanel.generategif.generateGif\nimport cn.netdiscovery.monica.ui.controlpanel.shapedrawing.shapeDrawing\nimport cn.netdiscovery.monica.ui.controlpanel.compression.compressionView\nimport cn.netdiscovery.monica.ui.controlpanel.webscreenshot.webScreenshot\nimport cn.netdiscovery.monica.ui.main.generalSettings\nimport cn.netdiscovery.monica.ui.main.mainView\nimport cn.netdiscovery.monica.ui.main.openURLDialog\nimport cn.netdiscovery.monica.ui.theme.CustomMaterialTheme\nimport cn.netdiscovery.monica.ui.main.showVersionInfo\nimport cn.netdiscovery.monica.ui.preview.PreviewViewModel\nimport cn.netdiscovery.monica.ui.showimage.showImage\nimport cn.netdiscovery.monica.ui.widget.PageLifecycle\nimport cn.netdiscovery.monica.ui.widget.showLoading\nimport cn.netdiscovery.monica.exception.ErrorHandler\nimport cn.netdiscovery.monica.exception.ErrorState\nimport cn.netdiscovery.monica.ui.widget.topToast\nimport cn.netdiscovery.monica.utils.chooseImage\nimport cn.netdiscovery.monica.utils.getBufferedImage\nimport cn.netdiscovery.monica.utils.captureFullScreen\nimport cn.netdiscovery.monica.utils.loadScreenshotToState\nimport cn.netdiscovery.monica.utils.getUrlFromClipboard\nimport cn.netdiscovery.monica.utils.loadWebScreenshotToState\nimport cn.netdiscovery.monica.ui.screenshot.showSwingScreenshotAreaSelector\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.launch\nimport org.koin.compose.KoinApplication\nimport org.koin.compose.koinInject\nimport org.koin.core.Koin\nimport org.slf4j.Logger\nimport org.slf4j.LoggerFactory\n\nval filterNames = mutableListOf<String>()\nval filterMaps = mutableMapOf<String, String>()\n\nvar loadingDisplay by mutableStateOf(false)\nvar openURLDialog by mutableStateOf(false)\nvar picUrl by mutableStateOf(\"\")\n\nvar showVersion by mutableStateOf(false)\nprivate var showTopToast by mutableStateOf(false)\nprivate var topToastMessage by mutableStateOf(\"\")\nvar showGeneralSettings by mutableStateOf(false)\nvar showScreenshotAreaSelector by mutableStateOf(false)\n\nlateinit var mAppKoin: Koin\n\nprivate val logger: Logger = LoggerFactory.getLogger(object : Any() {}.javaClass.enclosingClass)\n\nfun main() = application {\n    // 初始化配置管理器（必须在 ApplicationState 创建之前）\n    ConfigManager.initialize(rxCache)\n    ConfigDefinitions.initialize()\n    \n    val trayState = rememberTrayState()\n\n    val applicationState = rememberApplicationState(\n        rememberCoroutineScope(),\n        trayState\n    )\n    \n    // 全局错误处理状态\n    val errorState = remember { ErrorState() }\n\n    PageLifecycle(\n        onInit = {\n            logger.info(\"首页启动时初始化\")\n            initData(applicationState)\n        },\n        onDisposeEffect = {\n            logger.info(\"首页关闭\")\n\n            applicationState.clearImage()\n            EditHistoryCenter.clearAll()\n\n            logger.info(\"释放全部资源\")\n        }\n    )\n\n    lateinit var previewViewModel: PreviewViewModel\n\n    Tray(\n        state = trayState,\n        icon = painterResource(\"images/launcher.ico\"),\n        menu = {\n            Item(\n                text = LocalizationManager.getString(\"software_version_info\"),\n                onClick = {\n                    showVersion = true\n                },\n            )\n            Item(\n                text = LocalizationManager.getString(\"open_local_image\"),\n                onClick = {\n                    chooseImage(applicationState) { file ->\n                        val image = getBufferedImage(file, applicationState)\n                        applicationState.rawImage = image\n                        applicationState.currentImage = applicationState.rawImage\n                        applicationState.rawImageFile = file\n                    }\n                },\n            )\n            Item(\n                text = LocalizationManager.getString(\"load_network_image\"),\n                onClick = {\n                    openURLDialog = true\n                },\n            )\n            Separator()\n            Item(\n                text = LocalizationManager.getString(\"screenshot_full_screen\"),\n                onClick = {\n                    // 全屏截图：先隐藏主窗口，延迟截图，再恢复主窗口\n                    applicationState.window.isVisible = false\n\n                    applicationState.scope.launch {\n                        try {\n                            delay(200) // 等待窗口隐藏动画完成\n                            val screenshot = captureFullScreen()\n                            delay(100)\n                            // 在 AWT Event Dispatch Thread 中恢复窗口可见性\n                            java.awt.EventQueue.invokeLater {\n                                applicationState.window.isVisible = true\n                                if (screenshot != null) {\n                                    loadScreenshotToState(applicationState, screenshot)\n                                }\n                            }\n                        } catch (e: Exception) {\n                            logger.error(\"全屏截图失败\", e)\n                            // 确保窗口在错误时也能恢复\n                            java.awt.EventQueue.invokeLater {\n                                applicationState.window.isVisible = true\n                            }\n                        }\n                    }\n                },\n            )\n            Item(\n                text = LocalizationManager.getString(\"screenshot_area\"),\n                onClick = {\n                    // 区域选择：显示简化对话框，用户点击截图\n                    showScreenshotAreaSelector = true\n                },\n            )\n            Item(\n                text = LocalizationManager.getString(\"web_screenshot\"),\n                onClick = {\n                    // 网页长截图：从剪贴板读取 URL 并直接截图\n                    applicationState.scope.launch {\n                        try {\n                            val url = getUrlFromClipboard()\n                            if (url == null) {\n                                showTopToast(\"剪贴板中没有有效的 URL，请先复制网页地址\")\n                                return@launch\n                            }\n\n                            loadWebScreenshotToState(applicationState, url)\n                        } catch (e: Exception) {\n                            logger.error(\"网页截图失败\", e)\n                            showTopToast(\"网页截图失败: ${e.message}\")\n                        }\n                    }\n                },\n            )\n            Separator()\n            Item(\n                text = LocalizationManager.getString(\"save_image\"),\n                onClick = {\n                    previewViewModel.saveImage(applicationState)\n                },\n            )\n        }\n    )\n\n    Thread.setDefaultUncaughtExceptionHandler{ _, throwable ->\n        logger.error(\"全局异常捕获\", throwable)\n    }\n\n    Window(onCloseRequest = ::exitApplication,\n        title = \"${LocalizationManager.getString(\"monica_image_editor\")} $appVersion\",\n        state = rememberWindowState(width = width, height = height).apply {\n            position = WindowPosition(Alignment.BottomCenter)\n        }) {\n\n        KoinApplication(application = {\n            mAppKoin = koin\n            modules(viewModelModule)\n        }) {\n            previewViewModel         = koinInject()\n\n            applicationState.window  = window\n\n            CustomMaterialTheme(theme = applicationState.getCurrentThemeValue().also { \n                logger.info(\"主窗口使用主题: ${it.name}\")\n            }) {\n                mainView(applicationState)\n                \n                // 全局错误处理 - 在 mainView 之后，确保在最顶层\n                ErrorHandler(errorState)\n\n                if (loadingDisplay) {\n                    showLoading()\n                }\n\n                if (openURLDialog) {\n                    openURLDialog(\n                        onConfirm = {\n                            openURLDialog = false\n\n                            previewViewModel.loadUrl(picUrl, applicationState)\n\n                            picUrl = \"\"\n                        },\n                        onDismiss = {\n                            openURLDialog = false\n                        })\n                }\n\n                if (showTopToast) {\n                    topToast(message = topToastMessage) {\n                        showTopToast = false\n                    }\n                }\n\n                if (showVersion) {\n                    showVersionInfo {\n                        showVersion = false\n                    }\n                }\n\n                if (showGeneralSettings) {\n                    generalSettings(applicationState) {\n                        showGeneralSettings = false\n                    }\n                }\n            }\n        }\n    }\n\n    if (applicationState.isShowPreviewWindow) {\n\n        if (applicationState.currentImage == null &&\n            (applicationState.currentStatus != GenerateGifStatus\n                    && applicationState.currentStatus != CompressionStatus\n                    && applicationState.currentStatus != FilterStatus\n                    && applicationState.currentStatus != FaceSwapStatus\n                    && applicationState.currentStatus != OpenCVDebugStatus\n                    && applicationState.currentStatus != CartoonStatus\n                    && applicationState.currentStatus != WebScreenshotStatus)) {\n            showTopToast(\"请先选择图像\")\n\n            return@application\n        }\n\n        Window(\n            title = getWindowsTitle(applicationState),\n            onCloseRequest = {\n                when(applicationState.currentStatus) {\n                    DoodleStatus -> {\n                        showTopToast(\"想要保存涂鸦效果，需要点击保存按钮\")\n                    }\n                    ShapeDrawingStatus -> {\n                        showTopToast(\"想要保存形状绘制的结果，需要点击保存按钮\")\n                    }\n                }\n\n                applicationState.closePreviewWindow()\n            },\n            state = rememberWindowState().apply {\n                position = WindowPosition(Alignment.Center)\n                placement = if(isWindows) WindowPlacement.Maximized else WindowPlacement.Fullscreen\n            }\n        ) {\n            CustomMaterialTheme(theme = applicationState.getCurrentThemeValue().also { \n                logger.info(\"预览窗口使用主题: ${it.name}\")\n            }) {\n                when(applicationState.currentStatus) {\n                    ZoomPreviewStatus -> {\n                        logger.info(\"enter ShowImgView\")\n                        showImage(applicationState)\n                    }\n                    ColorPickStatus -> {\n                        logger.info(\"enter ColorPickView\")\n                        colorPick(applicationState)\n                    }\n                    GenerateGifStatus -> {\n                        logger.info(\"enter GenerateGifView\")\n                        generateGif(applicationState)\n                    }\n                    DoodleStatus -> {\n                        logger.info(\"enter DoodleView\")\n                        drawImage(applicationState)\n                    }\n                    ShapeDrawingStatus -> {\n                        logger.info(\"enter ShapeDrawingViewRefactored\")\n                        shapeDrawing(applicationState)\n                    }\n                    CropSizeStatus -> {\n                        logger.info(\"enter CropImageView\")\n                        cropImage(applicationState)\n                    }\n                    ColorCorrectionStatus -> {\n                        logger.info(\"enter ColorCorrectionView\")\n                        colorCorrection(applicationState)\n                    }\n                    FilterStatus -> {\n                        logger.info(\"enter FilterView\")\n                        filter(applicationState)\n                    }\n                    FaceSwapStatus -> {\n                        logger.info(\"enter FaceSwapView\")\n                        faceSwap(applicationState)\n                    }\n                    OpenCVDebugStatus -> {\n                        logger.info(\"enter OpenCVDebugView\")\n                        experiment(applicationState)\n                    }\n                    CartoonStatus -> {\n                        logger.info(\"enter CartoonView\")\n                        cartoon(applicationState)\n                    }\n                    CompressionStatus -> {\n                        logger.info(\"enter CompressionView\")\n                        compressionView(applicationState)\n                    }\n                    WebScreenshotStatus -> {\n                        logger.info(\"enter WebScreenshotView\")\n                        webScreenshot(applicationState)\n                    }\n                    else -> {}\n                }\n            }\n        }\n    }\n\n    if (showScreenshotAreaSelector) {\n        // 使用 Swing 实现的区域选择器（在 macOS 上更可靠）\n        showSwingScreenshotAreaSelector(\n            state = applicationState,\n            onDismiss = {\n                showScreenshotAreaSelector = false\n            }\n        )\n    }\n}\n\nfun showTopToast(message:String) {\n    topToastMessage = message\n    showTopToast = true\n}\n\n/**\n * 初始化数据，只初始一次，包括：\n * 1. 初始化配置定义\n * 2. 加载滤镜的配置\n * 3. 加载 opencv 的图像处理库\n * 4. 校验算法服务\n */\nprivate fun initData(state:ApplicationState) {\n\n    logger.info(\"os = $os, arch = $arch, osVersion = $osVersion, javaVersion = $javaVersion, javaVendor = $javaVendor, monicaVersion = $appVersion, kotlinVersion = $kotlinVersion\")\n\n    // 配置管理器已在 main() 函数开始处初始化，这里不再重复初始化\n\n    filterNames.addAll(getFilterNames()) // 获取所有滤镜的名称\n\n    if (rxCache.allKeys.isEmpty()) { // 第一次加载会缓存所有滤镜的参数配置\n        initFilterParamsConfig()\n    }\n\n    if (filterMaps.isEmpty()) {\n        initFilterMap()\n    }\n\n    logger.info(\"MonicaImageProcess Version = $imageProcessVersion, OpenCV Version = $openCVVersion\")\n\n    if (state.algorithmUrlText.isNotEmpty()) {\n\n        val status = try {\n            val baseUrl = state.algorithmUrlText\n            if (healthCheck(baseUrl)) {\n                STATUS_HTTP_SERVER_OK\n            } else {\n                STATUS_HTTP_SERVER_FAILED\n            }\n        } catch (e:Exception) {\n            STATUS_HTTP_SERVER_FAILED\n        }\n\n        if (status == STATUS_HTTP_SERVER_OK) {\n            logger.info(\"算法服务可用\")\n        } else {\n            logger.info(\"算法服务不可用\")\n        }\n    }\n}\n\nprivate fun getWindowsTitle(state: ApplicationState): String = when(state.currentStatus) {\n    DoodleStatus          -> LocalizationManager.getString(\"window_title_doodle\")\n    ShapeDrawingStatus    -> LocalizationManager.getString(\"window_title_shape_drawing\")\n    ColorPickStatus       -> LocalizationManager.getString(\"window_title_color_pick\")\n    GenerateGifStatus     -> LocalizationManager.getString(\"window_title_generate_gif\")\n    CropSizeStatus        -> LocalizationManager.getString(\"window_title_crop_size\")\n    ColorCorrectionStatus -> LocalizationManager.getString(\"window_title_color_correction\")\n    FilterStatus          -> LocalizationManager.getString(\"window_title_filter\")\n    OpenCVDebugStatus     -> LocalizationManager.getString(\"window_title_opencv_debug\")\n    FaceSwapStatus        -> LocalizationManager.getString(\"window_title_face_swap\")\n    CartoonStatus         -> LocalizationManager.getString(\"window_title_cartoon\")\n    CompressionStatus     -> LocalizationManager.getString(\"window_title_compression\")\n    WebScreenshotStatus   -> LocalizationManager.getString(\"window_title_web_screenshot\")\n    else                  -> LocalizationManager.getString(\"window_title_preview\")\n}\n"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/config/Constant.kt",
    "content": "package cn.netdiscovery.monica.config\n\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport cn.netdiscovery.monica.opencv.ImageProcess\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.config.Constant\n * @author: Tony Shen\n * @date: 2024/5/7 10:55\n * @version: V1.0 <描述当前版本功能>\n */\n\nval imageProcessVersion by lazy { // 本地算法库的版本\n    ImageProcess.getVersion()\n}\n\nval openCVVersion by lazy { // OpenCV 的版本\n    ImageProcess.getOpenCVVersion()\n}\n\nval previewWidth = 750\nval width = (previewWidth * 2.toFloat()).dp\nval height = 1000.dp\nval loadingWidth = (previewWidth*2*0.7).dp\n\nval titleTextSize = 32.sp\nval subTitleTextSize = 20.sp\n\n"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/di/appModule.kt",
    "content": "package cn.netdiscovery.monica.di\n\nimport org.koin.core.module.dsl.singleOf\nimport org.koin.dsl.module\nimport cn.netdiscovery.monica.ui.controlpanel.ai.AIViewModel\nimport cn.netdiscovery.monica.ui.controlpanel.ai.experiment.viewmodel.BinaryImageViewModel\nimport cn.netdiscovery.monica.ui.controlpanel.ai.experiment.viewmodel.ContourAnalysisViewModel\nimport cn.netdiscovery.monica.ui.controlpanel.ai.experiment.viewmodel.EdgeDetectionViewModel\nimport cn.netdiscovery.monica.ui.controlpanel.ai.experiment.viewmodel.HistoryViewModel\nimport cn.netdiscovery.monica.ui.controlpanel.ai.experiment.viewmodel.ImageEnhanceViewModel\nimport cn.netdiscovery.monica.ui.controlpanel.ai.experiment.viewmodel.ImageDenoisingViewModel\nimport cn.netdiscovery.monica.ui.controlpanel.ai.experiment.viewmodel.MatchTemplateViewModel\nimport cn.netdiscovery.monica.ui.controlpanel.ai.experiment.viewmodel.MorphologicalOperationsViewModel\nimport cn.netdiscovery.monica.ui.controlpanel.ai.faceswap.FaceSwapViewModel\nimport cn.netdiscovery.monica.ui.controlpanel.cartoon.CartoonViewModel\nimport cn.netdiscovery.monica.ui.controlpanel.colorcorrection.ColorCorrectionViewModel\nimport cn.netdiscovery.monica.ui.controlpanel.cropimage.CropViewModel\nimport cn.netdiscovery.monica.ui.controlpanel.doodle.DoodleViewModel\nimport cn.netdiscovery.monica.ui.controlpanel.filter.viewmodel.FilterViewModel\nimport cn.netdiscovery.monica.ui.controlpanel.generategif.GenerateGifViewModel\nimport cn.netdiscovery.monica.ui.controlpanel.shapedrawing.ShapeDrawingViewModel\nimport cn.netdiscovery.monica.ui.controlpanel.webscreenshot.WebScreenshotViewModel\nimport cn.netdiscovery.monica.ui.main.MainViewModel\nimport cn.netdiscovery.monica.ui.preview.PreviewViewModel\n\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.di.appModule\n * @author: Tony Shen\n * @date: 2024/5/7 20:28\n * @version: V1.0 <描述当前版本功能>\n */\nval viewModelModule = module {\n\n    singleOf(::MainViewModel)\n    singleOf(::PreviewViewModel)\n    singleOf(::DoodleViewModel)\n    singleOf(::ShapeDrawingViewModel)\n    singleOf(::CropViewModel)\n    singleOf(::ColorCorrectionViewModel)\n    singleOf(::FilterViewModel)\n    singleOf(::AIViewModel)\n    singleOf(::FaceSwapViewModel)\n    singleOf(::BinaryImageViewModel)\n    singleOf(::EdgeDetectionViewModel)\n    singleOf(::ContourAnalysisViewModel)\n    singleOf(::ImageEnhanceViewModel)\n    singleOf(::ImageDenoisingViewModel)\n    singleOf(::MorphologicalOperationsViewModel)\n    singleOf(::MatchTemplateViewModel)\n    singleOf(::HistoryViewModel)\n    singleOf(::GenerateGifViewModel)\n    singleOf(::CartoonViewModel)\n    singleOf(::WebScreenshotViewModel)\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/exception/AppError.kt",
    "content": "package cn.netdiscovery.monica.exception\n\n/**\n * 应用错误类\n * @FileName:\n *          cn.netdiscovery.monica.exception.AppError\n * @author: Tony Shen\n * @date: 2025/9/26 17:20\n * @version: V1.0 <描述当前版本功能>\n */\ndata class AppError(\n    val type: ErrorType,\n    val severity: ErrorSeverity,\n    val message: String,                              // 技术性错误信息，用于日志\n    val userMessage: String,                          // 用户友好的错误信息\n    val cause: Throwable? = null,                     // 原始异常\n    val retryable: Boolean = false,                   // 是否可重试\n    val context: Map<String, Any> = emptyMap(),       // 错误上下文信息\n    val timestamp: Long = System.currentTimeMillis(), // 错误发生时间\n    val errorCode: String? = null                     // 错误代码，用于国际化\n) {\n    companion object {\n        fun fromException(\n            exception: Throwable,\n            type: ErrorType,\n            severity: ErrorSeverity = ErrorSeverity.MEDIUM,\n            userMessage: String = \"操作失败，请重试\"\n        ): AppError {\n            return AppError(\n                type = type,\n                severity = severity,\n                message = exception.message ?: \"未知错误\",\n                userMessage = userMessage,\n                cause = exception,\n                retryable = isRetryableException(exception)\n            )\n        }\n\n        private fun isRetryableException(exception: Throwable): Boolean {\n            return when (exception) {\n                is java.net.SocketTimeoutException,\n                is java.net.ConnectException,\n                is java.io.IOException -> true\n                else -> false\n            }\n        }\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/exception/ErrorComposable.kt",
    "content": "package cn.netdiscovery.monica.exception\n\nimport androidx.compose.material.AlertDialog\nimport androidx.compose.material.Text\nimport androidx.compose.material.TextButton\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Modifier\nimport cn.netdiscovery.monica.ui.widget.centerToast\nimport cn.netdiscovery.monica.ui.widget.topToast\n\n/**\n * 错误处理Compose组件\n * @FileName:\n *          cn.netdiscovery.monica.exception.ErrorComposable\n * @author: Tony Shen\n * @date: 2025/9/28 10:40\n * @version: V1.0 <描述当前版本功能>\n */\n@Composable\nfun ErrorHandler(\n    errorState: ErrorState\n) {\n    // 初始化错误管理器\n    val errorManager = remember { \n        ErrorManager().apply {\n            setErrorState(errorState)\n            // 注册默认错误处理器\n            registerHandler(cn.netdiscovery.monica.exception.handlers.NetworkErrorHandler())\n            registerHandler(cn.netdiscovery.monica.exception.handlers.ImageProcessingErrorHandler())\n            registerHandler(cn.netdiscovery.monica.exception.handlers.ValidationErrorHandler())\n            registerHandler(cn.netdiscovery.monica.exception.handlers.FileIOErrorHandler())\n            registerHandler(cn.netdiscovery.monica.exception.handlers.AIServiceErrorHandler())\n        }\n    }\n    \n    // 设置全局错误管理器\n    LaunchedEffect(errorManager) {\n        GlobalErrorManager.setInstance(errorManager, errorState)\n    }\n    \n    // 监听 Toast 消息\n    val toastMessage by errorState.toastMessage.collectAsState()\n    // 显示 Toast\n    if (toastMessage != null) {\n        centerToast(\n            modifier = Modifier, \n            message = toastMessage!!,\n            onDismissCallback = {\n                errorState.clearToast()\n            }\n        )\n    }\n\n    // 监听 Top Toast 消息\n    val topToastMessage by errorState.topToastMessage.collectAsState()\n    // 显示 Top Toast\n    if (topToastMessage != null) {\n        topToast(\n            modifier = Modifier,\n            message = topToastMessage!!,\n            onDismissCallback = {\n                errorState.clearToast()\n            }\n        )\n    }\n\n    // 监听对话框状态\n    val dialogState by errorState.dialogState.collectAsState()\n    dialogState?.let { state ->\n        AlertDialog(\n            onDismissRequest = { \n                errorState.clearDialog()\n                state.onDismiss()\n            },\n            title = { Text(state.title) },\n            text = { Text(state.message) },\n            confirmButton = {\n                TextButton(onClick = { \n                    errorState.clearDialog()\n                    state.onDismiss()\n                }) {\n                    Text(\"确定\")\n                }\n            }\n        )\n    }\n}\n"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/exception/ErrorExtensions.kt",
    "content": "package cn.netdiscovery.monica.exception\n\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\n\n/**\n * 错误处理扩展函数\n * @FileName:\n *          cn.netdiscovery.monica.exception.ErrorExtensions\n * @author: Tony Shen\n * @date: 2025/9/28 10:35\n * @version: V1.0 <描述当前版本功能>\n */\n\n/**\n * 全局错误处理函数 - 用于在非 Composable 上下文中调用\n */\nfun showError(\n    type: ErrorType,\n    severity: ErrorSeverity = ErrorSeverity.MEDIUM,\n    message: String = \"操作失败，请重试\",\n    userMessage: String = \"操作失败，请重试\"\n) {\n    val error = AppError(\n        type = type,\n        severity = severity,\n        message = message,\n        userMessage = userMessage\n    )\n    val errorState = GlobalErrorManager.getErrorState()\n    if (errorState != null) {\n        errorState.showError(error)\n    } else {\n        // 如果 GlobalErrorManager 未初始化，直接打印日志\n        println(\"错误: ${error.userMessage}\")\n    }\n}\n\n/**\n * 安全执行IO操作\n */\nsuspend fun <T> safeExecuteIO(\n    errorType: ErrorType = ErrorType.FILE_IO_ERROR,\n    severity: ErrorSeverity = ErrorSeverity.MEDIUM,\n    userMessage: String = \"文件操作失败，请重试\",\n    retryable: Boolean = true,\n    context: Map<String, Any> = emptyMap(),\n    block: suspend () -> T\n): Result<T> {\n    return withContext(Dispatchers.IO) {\n        safeExecute(errorType, severity, userMessage, retryable, context, block)\n    }\n}\n/**\n * 安全执行异步操作\n */\nsuspend fun <T> safeExecute(\n    errorType: ErrorType,\n    severity: ErrorSeverity = ErrorSeverity.MEDIUM,\n    userMessage: String = \"操作失败，请重试\",\n    retryable: Boolean = false,\n    context: Map<String, Any> = emptyMap(),\n    block: suspend () -> T\n): Result<T> {\n    return try {\n        Result.Success(block())\n    } catch (e: Exception) {\n        val error = AppError(\n            type = errorType,\n            severity = severity,\n            message = e.message ?: \"未知错误\",\n            userMessage = userMessage,\n            cause = e,\n            retryable = retryable,\n            context = context\n        )\n        GlobalErrorManager.getErrorState()?.showError(error) ?: run {\n            // 如果全局错误管理器未初始化，仅记录日志\n            println(\"错误: ${error.message}\")\n        }\n        Result.Error(error)\n    }\n}\n\n/**\n * 安全执行同步操作\n */\nfun <T> safeExecuteSync(\n    errorType: ErrorType,\n    severity: ErrorSeverity = ErrorSeverity.MEDIUM,\n    userMessage: String = \"操作失败，请重试\",\n    retryable: Boolean = false,\n    context: Map<String, Any> = emptyMap(),\n    block: () -> T\n): Result<T> {\n    return try {\n        Result.Success(block())\n    } catch (e: Exception) {\n        val error = AppError(\n            type = errorType,\n            severity = severity,\n            message = e.message ?: \"未知错误\",\n            userMessage = userMessage,\n            cause = e,\n            retryable = retryable,\n            context = context\n        )\n        GlobalErrorManager.getErrorState()?.showError(error) ?: run {\n            // 如果全局错误管理器未初始化，仅记录日志\n            println(\"错误: ${error.message}\")\n        }\n        Result.Error(error)\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/exception/ErrorHandler.kt",
    "content": "package cn.netdiscovery.monica.exception\n\n/**\n * 错误处理器接口\n * @FileName:\n *          cn.netdiscovery.monica.exception.ErrorHandler\n * @author: Tony Shen\n * @date: 2025/9/26 17:15\n * @version: V1.0 <描述当前版本功能>\n */\ninterface ErrorHandler {\n\n    fun handleError(error: AppError): ErrorHandlingStrategy\n\n    fun canHandle(errorType: ErrorType): Boolean\n}\n"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/exception/ErrorManager.kt",
    "content": "package cn.netdiscovery.monica.exception\n\nimport cn.netdiscovery.monica.utils.logger\n\n/**\n * 错误管理器\n * @FileName:\n *          cn.netdiscovery.monica.exception.ErrorManager\n * @author: Tony Shen\n * @date: 2025/9/26 17:32\n * @version: V1.0 <描述当前版本功能>\n */\n\n// 全局错误管理器实例\nobject GlobalErrorManager {\n    private var _instance: ErrorManager? = null\n    private var _errorState: ErrorState? = null\n\n    fun setInstance(errorManager: ErrorManager, errorState: ErrorState) {\n        _instance = errorManager\n        _errorState = errorState\n    }\n\n    fun getInstance(): ErrorManager? = _instance\n    \n    fun getErrorState(): ErrorState? = _errorState\n}\n\nclass ErrorManager {\n    private val handlers = mutableListOf<ErrorHandler>()\n    private val logger = logger<ErrorManager>()\n    private var errorState: ErrorState? = null\n\n    fun setErrorState(errorState: ErrorState) {\n        this.errorState = errorState\n    }\n\n    fun registerHandler(handler: ErrorHandler) {\n        handlers.add(handler)\n    }\n\n    fun handleError(error: AppError) {\n        // 记录错误日志\n        logError(error)\n\n        // 查找合适的处理器\n        val handler = handlers.find { it.canHandle(error.type) }\n\n        // 根据处理策略更新状态\n        when (handler?.handleError(error)) {\n            ErrorHandlingStrategy.SHOW_TOAST -> {\n                errorState?.showToast(error.userMessage)\n            }\n            ErrorHandlingStrategy.SHOW_DIALOG -> {\n                errorState?.showDialog(\"错误\", error.userMessage)\n            }\n            ErrorHandlingStrategy.RETRY -> {\n                // 重试逻辑\n            }\n            ErrorHandlingStrategy.LOG_ONLY -> {\n                // 仅记录日志\n            }\n            else -> logger.warn(\"未处理的错误: ${error.message}\")\n        }\n    }\n\n    private fun logError(error: AppError) {\n        when (error.severity) {\n            ErrorSeverity.LOW -> logger.debug(\"${error.type}: ${error.message}\", error.cause)\n            ErrorSeverity.MEDIUM -> logger.warn(\"${error.type}: ${error.message}\", error.cause)\n            ErrorSeverity.HIGH -> logger.error(\"${error.type}: ${error.message}\", error.cause)\n            ErrorSeverity.CRITICAL -> logger.error(\"严重错误 [${error.type}]: ${error.message}\", error.cause)\n        }\n    }\n}\n"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/exception/ErrorState.kt",
    "content": "package cn.netdiscovery.monica.exception\n\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.asStateFlow\n\n/**\n * 错误状态管理\n * @FileName:\n *          cn.netdiscovery.monica.exception.ErrorState\n * @author: Tony Shen\n * @date: 2025/9/28 10:17\n * @version: V1.0 <描述当前版本功能>\n */\nclass ErrorState {\n    private val _toastMessage = MutableStateFlow<String?>(null)\n    val toastMessage: StateFlow<String?> = _toastMessage.asStateFlow()\n\n    private val _topToastMessage = MutableStateFlow<String?>(null)\n    val topToastMessage: StateFlow<String?> = _topToastMessage.asStateFlow()\n\n    private val _dialogState = MutableStateFlow<DialogState?>(null)\n    val dialogState: StateFlow<DialogState?> = _dialogState.asStateFlow()\n\n    data class DialogState(\n        val title: String,\n        val message: String,\n        val onDismiss: () -> Unit\n    )\n\n    fun showToast(message: String) {\n        _toastMessage.value = message\n    }\n\n    fun showTopToast(message: String) {\n        _topToastMessage.value = message\n    }\n\n    fun showDialog(title: String, message: String, onDismiss: () -> Unit = {}) {\n        _dialogState.value = DialogState(title, message, onDismiss)\n    }\n\n    fun clearToast() {\n        _toastMessage.value = null\n        _topToastMessage.value = null\n    }\n\n    fun clearDialog() {\n        _dialogState.value = null\n    }\n    \n    /**\n     * 直接显示错误 - 用于在非 Composable 上下文中调用\n     */\n    fun showError(error: AppError) {\n        when (error.severity) {\n            ErrorSeverity.LOW -> {\n                showTopToast(error.userMessage)\n            }\n            ErrorSeverity.MEDIUM -> {\n                showToast(error.userMessage)\n            }\n            ErrorSeverity.HIGH -> {\n                showDialog(\"错误\", error.userMessage)\n            }\n            ErrorSeverity.CRITICAL -> {\n                showDialog(\"严重错误\", error.userMessage)\n            }\n        }\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/exception/Errors.kt",
    "content": "package cn.netdiscovery.monica.exception\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.exception.Errors\n * @author: Tony Shen\n * @date: 2025/9/26 17:12\n * @version: V1.0 <描述当前版本功能>\n */\n// 错误类型枚举\nenum class ErrorType {\n    NETWORK_ERROR,      // 网络错误\n    IMAGE_PROCESSING,   // 图像处理错误\n    VALIDATION_ERROR,   // 验证错误\n    FILE_IO_ERROR,      // 文件IO错误\n    CONFIG_ERROR,       // 配置错误\n    AI_SERVICE_ERROR,   // AI服务错误\n    UI_ERROR,           // UI错误\n    UNKNOWN_ERROR       // 未知错误\n}\n\n// 错误严重程度\nenum class ErrorSeverity {\n    LOW,        // 低严重程度，不影响主要功能\n    MEDIUM,     // 中等严重程度，影响部分功能\n    HIGH,       // 高严重程度，影响主要功能\n    CRITICAL    // 严重错误，可能导致应用崩溃\n}\n\n// 错误处理策略\nenum class ErrorHandlingStrategy {\n    SHOW_TOAST,         // 显示Toast提示\n    SHOW_DIALOG,        // 显示错误对话框\n    LOG_ONLY,           // 仅记录日志\n    RETRY,              // 自动重试\n    FALLBACK,           // 降级处理\n    IGNORE              // 忽略错误\n}\n\n// 统一错误结果\nsealed class Result<T> {\n    data class Success<T>(val data: T) : Result<T>()\n    data class Error<T>(val error: AppError) : Result<T>()\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/exception/MonicaException.kt",
    "content": "package cn.netdiscovery.monica.exception\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.exception.MonicaException\n * @author: Tony Shen\n * @date: 2025/6/9 11:25\n * @version: V1.0 <描述当前版本功能>\n */\nclass MonicaException : RuntimeException {\n    constructor() : super()\n\n    constructor(message: String?, cause: Throwable?) : super(message, cause)\n\n    constructor(message: String?) : super(message)\n\n    constructor(cause: Throwable?) : super(cause)\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/exception/handlers/AIServiceErrorHandler.kt",
    "content": "package cn.netdiscovery.monica.exception.handlers\n\nimport cn.netdiscovery.monica.exception.*\n\n/**\n * AI服务错误处理器\n * @FileName:\n *          cn.netdiscovery.monica.exception.handlers.AIServiceErrorHandler\n * @author: Tony Shen\n * @date: 2025/9/28 10:30\n * @version: V1.0 <描述当前版本功能>\n */\nclass AIServiceErrorHandler : ErrorHandler {\n    \n    override fun handleError(error: AppError): ErrorHandlingStrategy {\n        return when (error.severity) {\n            ErrorSeverity.LOW -> ErrorHandlingStrategy.SHOW_TOAST\n            ErrorSeverity.MEDIUM -> ErrorHandlingStrategy.SHOW_DIALOG\n            ErrorSeverity.HIGH -> if (error.retryable) ErrorHandlingStrategy.RETRY else ErrorHandlingStrategy.SHOW_DIALOG\n            ErrorSeverity.CRITICAL -> ErrorHandlingStrategy.FALLBACK\n        }\n    }\n    \n    override fun canHandle(errorType: ErrorType): Boolean = \n        errorType == ErrorType.AI_SERVICE_ERROR\n}\n"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/exception/handlers/FileIOErrorHandler.kt",
    "content": "package cn.netdiscovery.monica.exception.handlers\n\nimport cn.netdiscovery.monica.exception.*\n\n/**\n * 文件IO错误处理器\n * @FileName:\n *          cn.netdiscovery.monica.exception.handlers.FileIOErrorHandler\n * @author: Tony Shen\n * @date: 2025/9/28 10:30\n * @version: V1.0 <描述当前版本功能>\n */\nclass FileIOErrorHandler : ErrorHandler {\n    \n    override fun handleError(error: AppError): ErrorHandlingStrategy {\n        return when (error.severity) {\n            ErrorSeverity.LOW -> ErrorHandlingStrategy.SHOW_TOAST\n            ErrorSeverity.MEDIUM -> ErrorHandlingStrategy.SHOW_DIALOG\n            ErrorSeverity.HIGH -> ErrorHandlingStrategy.SHOW_DIALOG\n            ErrorSeverity.CRITICAL -> ErrorHandlingStrategy.FALLBACK\n        }\n    }\n    \n    override fun canHandle(errorType: ErrorType): Boolean = \n        errorType == ErrorType.FILE_IO_ERROR\n}\n"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/exception/handlers/ImageProcessingErrorHandler.kt",
    "content": "package cn.netdiscovery.monica.exception.handlers\n\nimport cn.netdiscovery.monica.exception.*\n\n/**\n * 图像处理错误处理器\n * @FileName:\n *          cn.netdiscovery.monica.exception.handlers.ImageProcessingErrorHandler\n * @author: Tony Shen\n * @date: 2025/9/28 10:30\n * @version: V1.0 <描述当前版本功能>\n */\nclass ImageProcessingErrorHandler : ErrorHandler {\n    \n    override fun handleError(error: AppError): ErrorHandlingStrategy {\n        return when (error.severity) {\n            ErrorSeverity.LOW -> ErrorHandlingStrategy.SHOW_TOAST\n            ErrorSeverity.MEDIUM -> ErrorHandlingStrategy.SHOW_DIALOG\n            ErrorSeverity.HIGH -> ErrorHandlingStrategy.SHOW_DIALOG\n            ErrorSeverity.CRITICAL -> ErrorHandlingStrategy.FALLBACK\n        }\n    }\n    \n    override fun canHandle(errorType: ErrorType): Boolean = \n        errorType == ErrorType.IMAGE_PROCESSING\n}\n"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/exception/handlers/NetworkErrorHandler.kt",
    "content": "package cn.netdiscovery.monica.exception.handlers\n\nimport cn.netdiscovery.monica.exception.*\n\n/**\n * 网络错误处理器\n * @FileName:\n *          cn.netdiscovery.monica.exception.handlers.NetworkErrorHandler\n * @author: Tony Shen\n * @date: 2025/9/28 10:30\n * @version: V1.0 <描述当前版本功能>\n */\nclass NetworkErrorHandler : ErrorHandler {\n    \n    override fun handleError(error: AppError): ErrorHandlingStrategy {\n        return when (error.severity) {\n            ErrorSeverity.LOW -> ErrorHandlingStrategy.SHOW_TOAST\n            ErrorSeverity.MEDIUM -> ErrorHandlingStrategy.SHOW_DIALOG\n            ErrorSeverity.HIGH -> if (error.retryable) ErrorHandlingStrategy.RETRY else ErrorHandlingStrategy.SHOW_DIALOG\n            ErrorSeverity.CRITICAL -> ErrorHandlingStrategy.FALLBACK\n        }\n    }\n    \n    override fun canHandle(errorType: ErrorType): Boolean = \n        errorType == ErrorType.NETWORK_ERROR\n}\n"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/exception/handlers/ValidationErrorHandler.kt",
    "content": "package cn.netdiscovery.monica.exception.handlers\n\nimport cn.netdiscovery.monica.exception.*\n\n/**\n * 验证错误处理器\n * @FileName:\n *          cn.netdiscovery.monica.exception.handlers.ValidationErrorHandler\n * @author: Tony Shen\n * @date: 2025/9/28 10:30\n * @version: V1.0 <描述当前版本功能>\n */\nclass ValidationErrorHandler : ErrorHandler {\n    \n    override fun handleError(error: AppError): ErrorHandlingStrategy {\n        return when (error.severity) {\n            ErrorSeverity.LOW -> ErrorHandlingStrategy.SHOW_TOAST\n            ErrorSeverity.MEDIUM -> ErrorHandlingStrategy.SHOW_TOAST\n            ErrorSeverity.HIGH -> ErrorHandlingStrategy.SHOW_DIALOG\n            ErrorSeverity.CRITICAL -> ErrorHandlingStrategy.SHOW_DIALOG\n        }\n    }\n    \n    override fun canHandle(errorType: ErrorType): Boolean = \n        errorType == ErrorType.VALIDATION_ERROR\n}\n"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/history/EditHistoryCenter.kt",
    "content": "package cn.netdiscovery.monica.history\n\nimport cn.netdiscovery.monica.config.KEY_GENERAL_SETTINGS\nimport cn.netdiscovery.monica.config.category.ConfigCategoryManager\nimport cn.netdiscovery.monica.domain.GeneralSettings\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.history.EditHistoryCenter\n * @author: Tony Shen\n * @date: 2025/7/28 13:47\n * @version: V1.0 全局的编辑历史协调器，用于管理多个模块的历史记录，例如图像调整、涂鸦 等。\n */\nobject EditHistoryCenter {\n\n    private val historyMap = mutableMapOf<String, EditHistoryManager<Any>>()\n    \n    /**\n     * 获取 GeneralSettings.maxHistorySize，带默认值\n     */\n    private fun getMaxHistorySize(): Int {\n        val defaultSettings = GeneralSettings(255, 255, 255, 512, 50, \"\", \"\", \"\", \"LIGHT\")\n        val settings = ConfigCategoryManager.load(KEY_GENERAL_SETTINGS, defaultSettings)\n        return settings.maxHistorySize\n    }\n\n    @Suppress(\"UNCHECKED_CAST\")\n    fun <T> getManager(key: String, scope: CoroutineScope? = null): EditHistoryManager<T> {\n        return historyMap.getOrPut(key) {\n            val maxHistorySize = getMaxHistorySize()\n            EditHistoryManager<Any>(maxHistorySize=maxHistorySize, coroutineScope = scope ?: CoroutineScope(Dispatchers.Default))\n        } as EditHistoryManager<T>\n    }\n\n    fun clearAll() {\n        historyMap.values.forEach { it.clear() }\n        historyMap.clear()\n    }\n\n    fun remove(key: String) {\n        historyMap.remove(key)\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/history/EditHistoryManager.kt",
    "content": "package cn.netdiscovery.monica.history\n\nimport cn.netdiscovery.monica.utils.logger\nimport kotlinx.coroutines.*\nimport org.slf4j.Logger\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.history.EditHistoryManager\n * @author: Tony Shen\n * @date: 2025/7/28 13:40\n * @version: V1.0 管理每个编辑会话的历史记录栈，包括撤销和重做功能。\n */\nclass EditHistoryManager<T>(\n    private val maxHistorySize: Int = 20,\n    private val coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.Default)\n) {\n\n    private val logger: Logger = logger<EditHistoryManager<T>>()\n\n    private val undoStack = ArrayDeque<Pair<T, HistoryEntry>>() // 历史状态栈\n    private val redoStack = ArrayDeque<Pair<T, HistoryEntry>>() // 可重做的状态\n    private val operationLog = mutableListOf<HistoryEntry>()     // 日志记录\n\n    val canUndo: Boolean get() = undoStack.size > 1              // 至少有一个可撤销\n    val canRedo: Boolean get() = redoStack.isNotEmpty()\n\n    private var debounceJob: Job? = null\n    private val debounceDelayMillis = 300L\n\n    /**\n     * 清空所有历史记录\n     */\n    fun clear() {\n        undoStack.clear()\n        redoStack.clear()\n        operationLog.clear()\n    }\n\n    /**\n     * 记录一次新的编辑状态。\n     * 新状态会成为当前状态，并清空 redo 栈。\n     */\n    fun push(state: T, entry: HistoryEntry) {\n        // 避免重复状态 (例如连续相同参数的调色)\n        val last = undoStack.lastOrNull()?.first\n        if (last != null && last == state) {\n            return\n        }\n\n        // 超出容量则移除最早的\n        if (undoStack.size >= maxHistorySize) {\n            undoStack.removeFirst()\n        }\n\n        undoStack.addLast(state to entry)\n        redoStack.clear()\n    }\n\n    /**\n     * 只记录操作日志（不会影响撤销栈）\n     */\n    fun logOnly(entry: HistoryEntry) {\n        operationLog.add(entry)\n        if (operationLog.size > maxHistorySize) {\n            operationLog.removeAt(0)\n        }\n    }\n\n    fun getOperationLog(): List<HistoryEntry> = operationLog.toList()\n\n    /**\n     * 防抖 push，避免频繁记录。\n     */\n    fun pushDebouncedAsync(\n        entry: HistoryEntry,\n        block: suspend () -> T,\n        onError: ((Throwable) -> Unit)? = null\n    ) {\n        debounceJob?.cancel()\n        debounceJob = coroutineScope.launch {\n            delay(debounceDelayMillis)\n            try {\n                val state = block()\n                withContext(Dispatchers.Main) {\n                    push(state, entry)\n                }\n            } catch (e: CancellationException) {\n                throw e\n            } catch (e: Exception) {\n                logger.error(\"pushDebouncedAsync failed: ${e.message}\", e)\n                onError?.invoke(e)\n            }\n        }\n    }\n\n    /**\n     * 撤销一次操作，返回撤销后的状态（上一个状态）。\n     */\n    fun undo(): Pair<T, HistoryEntry>? {\n        if (canUndo) {\n            val last = undoStack.removeLast()\n            redoStack.addLast(last)\n            return undoStack.lastOrNull()\n        }\n        return null\n    }\n\n    /**\n     * 重做（恢复上一次撤销的状态）\n     */\n    fun redo(): Pair<T, HistoryEntry>? {\n        if (canRedo) {\n            val next = redoStack.removeLast()\n            undoStack.addLast(next)\n            return next\n        }\n        return null\n    }\n\n    /**\n     * 查看上一个状态（不修改当前指针）\n     */\n    fun previousState(): Pair<T, HistoryEntry>? {\n        return if (canUndo) {\n            val iterator = undoStack.iterator()\n            var prev: Pair<T, HistoryEntry>? = null\n            while (iterator.hasNext()) {\n                val current = iterator.next()\n                if (!iterator.hasNext()) break // 到最后一个时退出\n                prev = current\n            }\n            prev\n        } else null\n    }\n\n    fun peekUndoEntry(): HistoryEntry? = undoStack.lastOrNull()?.second\n    fun peekRedoEntry(): HistoryEntry? = redoStack.lastOrNull()?.second\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/history/HistoryEntry.kt",
    "content": "package cn.netdiscovery.monica.history\n\nimport java.util.*\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.history.HistoryEntry\n * @author: Tony Shen\n * @date:  2025/7/26 10:21\n * @version: V1.0 记录对象\n */\ndata class HistoryEntry(\n    val id: String = UUID.randomUUID().toString(),\n    val timestamp: Long = System.currentTimeMillis(),\n    val module: String,\n    val operation: String,\n    val parameters: Map<String, Any>,\n    val previewImagePath: String = \"\",\n    val sourceImageHash: String = \"\",\n    val description: String = \"\"\n)"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/history/modules/colorcorrection/ColorCorrectionParams.kt",
    "content": "package cn.netdiscovery.monica.history.modules.colorcorrection\n\nimport cn.netdiscovery.monica.config.MODULE_COLOR\nimport cn.netdiscovery.monica.domain.ColorCorrectionSettings\nimport cn.netdiscovery.monica.history.EditHistoryManager\nimport cn.netdiscovery.monica.history.HistoryEntry\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.history.modules.colorcorrection.ColorCorrectionParams\n * @author: Tony Shen\n * @date:  2025/7/27 13:16\n * @version: V1.0 调色参数封装\n */\ndata class ColorCorrectionParams(\n    val contrast: Int = 255,\n    val hue: Int = 180,\n    val saturation: Int = 255,\n    val lightness: Int = 255,\n    val temperature: Int = 255,\n    val highlight: Int = 255,\n    val shadow: Int = 255,\n    val sharpen: Int = 0,\n    val corner: Int = 0,\n    val status: Int = 0 // 1 ~ 9 表示最近调整项，可选\n) {\n\n    fun toMap(): Map<String, Any> = mapOf(\n        \"contrast\" to contrast,\n        \"hue\" to hue,\n        \"saturation\" to saturation,\n        \"lightness\" to lightness,\n        \"temperature\" to temperature,\n        \"highlight\" to highlight,\n        \"shadow\" to shadow,\n        \"sharpen\" to sharpen,\n        \"corner\" to corner,\n        \"status\" to status\n    )\n\n    fun toSettings(): ColorCorrectionSettings = ColorCorrectionSettings(\n        contrast, hue, saturation, lightness, temperature,\n        highlight, shadow, sharpen, corner, status\n    )\n\n    companion object {\n        fun fromMap(map: Map<String, Any>): ColorCorrectionParams = ColorCorrectionParams(\n            contrast = (map[\"contrast\"] as? Number)?.toInt() ?: 255,\n            hue = (map[\"hue\"] as? Number)?.toInt() ?: 180,\n            saturation = (map[\"saturation\"] as? Number)?.toInt() ?: 255,\n            lightness = (map[\"lightness\"] as? Number)?.toInt() ?: 255,\n            temperature = (map[\"temperature\"] as? Number)?.toInt() ?: 255,\n            highlight = (map[\"highlight\"] as? Number)?.toInt() ?: 255,\n            shadow = (map[\"shadow\"] as? Number)?.toInt() ?: 255,\n            sharpen = (map[\"sharpen\"] as? Number)?.toInt() ?: 0,\n            corner = (map[\"corner\"] as? Number)?.toInt() ?: 0,\n            status = (map[\"status\"] as? Number)?.toInt() ?: 0\n        )\n\n        fun fromSettings(settings: ColorCorrectionSettings): ColorCorrectionParams =\n            ColorCorrectionParams(\n                contrast = settings.contrast,\n                hue = settings.hue,\n                saturation = settings.saturation,\n                lightness = settings.lightness,\n                temperature = settings.temperature,\n                highlight = settings.highlight,\n                shadow = settings.shadow,\n                sharpen = settings.sharpen,\n                corner = settings.corner,\n                status = settings.status\n            )\n    }\n}\n\nfun <T> EditHistoryManager<T>.recordColorCorrection(\n    module: String = MODULE_COLOR,\n    operation: String,\n    description: String = \"\",\n    isPush: Boolean = true,\n    colorCorrectionSettings: ColorCorrectionSettings\n) {\n    val params = ColorCorrectionParams.fromSettings(colorCorrectionSettings)\n    val entry = HistoryEntry(module = module, operation = operation, parameters = params.toMap(), description = description)\n    if (isPush) {\n        push(params as T, entry)\n    }\n\n    logOnly(entry)\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/history/modules/opencv/CVParams.kt",
    "content": "package cn.netdiscovery.monica.history.modules.opencv\n\nimport cn.netdiscovery.monica.config.MODULE_OPENCV\nimport cn.netdiscovery.monica.history.EditHistoryManager\nimport cn.netdiscovery.monica.history.HistoryEntry\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.history.modules.opencv.CVParams\n * @author: Tony Shen\n * @date: 2025/7/29 17:17\n * @version: V1.0 <描述当前版本功能>\n */\ndata class CVParams(\n    val operation: String = \"\",\n    val parameters: MutableMap<String, Any> = HashMap()\n)\n\nfun <T> EditHistoryManager<T>.recordCVOperation(\n    module: String = MODULE_OPENCV,\n    operation: String,\n    description: String = \"\",\n    buildParams: CVParams.() -> Unit\n) {\n    val params = CVParams(operation).apply(buildParams)\n    val entry = HistoryEntry(module = module, operation = operation, parameters = params.parameters, description = description)\n    push(params as T, entry)\n    logOnly(entry)\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/http/GsonSerializer.kt",
    "content": "package cn.netdiscovery.monica.http\n\nimport cn.netdiscovery.http.core.serializer.Serializer\nimport com.google.gson.Gson\nimport java.lang.reflect.Type\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.http.GsonSerializer\n * @author: Tony Shen\n * @date: 2025/4/3 18:58\n * @version: V1.0 <描述当前版本功能>\n */\nclass GsonSerializer: Serializer {\n\n    private val gson: Gson = Gson()\n\n    override fun <T> fromJson(json: String, type: Type): T = gson.fromJson(json,type)\n\n    override fun toJson(data: Any): String = gson.toJson(data)\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/http/HttpClient.kt",
    "content": "package cn.netdiscovery.monica.http\n\nimport cn.netdiscovery.http.core.HttpClientBuilder\nimport cn.netdiscovery.http.core.request.converter.GlobalRequestJSONConverter\nimport cn.netdiscovery.http.core.response.StringResponseMapper\nimport cn.netdiscovery.http.core.utils.extension.asyncCall\nimport cn.netdiscovery.http.interceptor.LoggingInterceptor\nimport cn.netdiscovery.http.interceptor.log.LogManager\nimport cn.netdiscovery.http.interceptor.log.LogProxy\nimport cn.netdiscovery.monica.utils.CVFailure\nimport cn.netdiscovery.monica.utils.CVSuccess\nimport okhttp3.MediaType\nimport okhttp3.MediaType.Companion.toMediaTypeOrNull\nimport okhttp3.Request\nimport okhttp3.RequestBody\nimport okio.BufferedSink\nimport org.slf4j.Logger\nimport org.slf4j.LoggerFactory\nimport java.awt.image.BufferedImage\nimport java.io.ByteArrayInputStream\nimport java.io.IOException\nimport java.util.concurrent.TimeUnit\nimport javax.imageio.ImageIO\n\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.http.HttpClient\n * @author: Tony Shen\n * @date: 2025/3/26 16:45\n * @version: V1.0 <描述当前版本功能>\n */\n\nconst val DEFAULT_CONN_TIMEOUT = 30\n\nprivate val logger: Logger = LoggerFactory.getLogger(object : Any() {}.javaClass.enclosingClass)\n\nval loggingInterceptor by lazy {\n    LogManager.logProxy(object : LogProxy {  // 必须要实现 LogProxy ，否则无法打印网络请求的 request 、response\n        override fun e(tag: String, msg: String) {\n        }\n\n        override fun w(tag: String, msg: String) {\n        }\n\n        override fun i(tag: String, msg: String) {\n            logger.info(\"$tag:$msg\")\n        }\n\n        override fun d(tag: String, msg: String) {\n            logger.info(\"$tag:$msg\")\n        }\n    })\n\n    LoggingInterceptor.Builder()\n        .loggable(true) // TODO: 发布到生产环境需要改成false\n        .request()\n        .requestTag(\"Request\")\n        .response()\n        .responseTag(\"Response\")\n//        .hideVerticalLine()// 隐藏竖线边框\n        .build()\n}\n\nval httpClient by lazy {\n    HttpClientBuilder()\n        .allTimeouts(DEFAULT_CONN_TIMEOUT.toLong(), TimeUnit.SECONDS)\n        .addInterceptor(loggingInterceptor)\n        .serializer(GsonSerializer())\n        .jsonConverter(GlobalRequestJSONConverter::class)\n        .responseMapper(StringResponseMapper::class)\n        .build()\n}\n\nfun healthCheck(baseUrl:String):Boolean = httpClient.get(url = \"${baseUrl}health\").code == 200\n\n/**\n * 封装 RequestBody\n */\nfun createRequestBody(image: BufferedImage, format:String): RequestBody {\n    return object : RequestBody() {\n        override fun contentType(): MediaType? {\n            return \"image/jpeg\".toMediaTypeOrNull()\n        }\n\n        override fun writeTo(sink: BufferedSink) {\n            // 使用 try-with-resources 确保流关闭\n            val outputStream = sink.outputStream()\n            outputStream.use {\n\n                if (!ImageIO.write(image, format, it)) {\n                    throw IOException(\"Unsupported image format: $format\")\n                }\n            }\n        }\n    }\n}\n\n/**\n * 封装 http 请求\n */\nfun createRequest(request: ()->Request,\n                  success: CVSuccess,\n                  failure: CVFailure) {\n\n    try {\n        httpClient.okHttpClient()\n            .asyncCall { request.invoke() }\n            .get()\n            .use { response->\n\n                val bufferedImage = ByteArrayInputStream(response.body?.bytes()).use { inputStream -> ImageIO.read(inputStream) }\n                success.invoke(bufferedImage)\n            }\n    } catch (e:Exception){\n        e.printStackTrace()\n        failure.invoke(e)\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/llm/DeepSeekRequest.kt",
    "content": "package cn.netdiscovery.monica.llm\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.llm.DeepSeekRequest\n * @author: Tony Shen\n * @date: 2025/8/2 17:08\n * @version: V1.0 <描述当前版本功能>\n */\ndata class DeepSeekRequest(\n    val model: String,\n    val messages: List<DeepSeekMessage>,\n    val stream: Boolean\n)\n\ndata class DeepSeekMessage(\n    val role: String,\n    val content: String\n)"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/llm/DeepseekClient.kt",
    "content": "package cn.netdiscovery.monica.llm\n\nimport cn.netdiscovery.http.core.utils.extension.asyncCall\nimport cn.netdiscovery.monica.domain.ColorCorrectionSettings\nimport cn.netdiscovery.monica.exception.MonicaException\nimport cn.netdiscovery.monica.http.httpClient\nimport com.safframework.rxcache.utils.GsonUtils\nimport kotlinx.coroutines.runBlocking\nimport okhttp3.Headers.Companion.toHeaders\nimport okhttp3.MediaType.Companion.toMediaTypeOrNull\nimport okhttp3.Request\nimport okhttp3.RequestBody\nimport org.json.JSONObject\nimport org.slf4j.Logger\nimport org.slf4j.LoggerFactory\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.llm.DeepseekClient\n * @author: Tony Shen\n * @date: 2025/8/1 13:59\n * @version: V1.0 <描述当前版本功能>\n */\nprivate val logger: Logger = LoggerFactory.getLogger(object : Any() {}.javaClass.enclosingClass)\n\n/**\n * 每次根据当前参数和新指令拼 prompt， 支持多轮对话\n */\n@Throws(MonicaException::class)\nfun applyInstructionWithLLM(\n    session: DialogSession,\n    instruction: String,\n    apiKey: String\n): ColorCorrectionSettings? {\n    val prompt = buildString {\n        append(\"当前图像的参数如下：\\n\")\n        append(GsonUtils.toJson(session.currentSettings))\n        append(\"\\n\\n\")\n        append(\"用户指令：$instruction\")\n    }\n    val messages = mutableListOf<DeepSeekMessage>().apply {\n        this.add(DeepSeekMessage(role = \"system\", content = systemPromptForColorCorrection))\n        this.add(DeepSeekMessage(role = \"user\", content = prompt))\n    }\n    val deepSeekRequest = DeepSeekRequest(\"deepseek-chat\", messages, false)\n    val payload = GsonUtils.toJson(deepSeekRequest)\n\n    val responseJson = sendPostJson(\n        url = DEEPSEEK_URL,\n        headers = mapOf(\"Content-Type\" to \"application/json\",\n            \"Authorization\" to \"Bearer $apiKey\"),\n        body = payload\n    )\n\n    try {\n        val json = extractJson(responseJson)\n        val responseObj = GsonUtils.fromJson<ColorCorrectionSettings>(json, ColorCorrectionSettings::class.java)\n\n        session.currentSettings = responseObj\n        // 历史记录现在在 LLMServiceManager 中处理\n        return responseObj\n    } catch (e: Exception) {\n        logger.error(\"responseJson = $responseJson\")\n        logger.error(e.message, e)\n        throw MonicaException(\"无法获取调色的参数\")\n    }\n}\n\nfun extractJson(jsonData: String): String {\n    val jsonObject: JSONObject = JSONObject(jsonData)\n    var content = jsonObject.getJSONArray(\"choices\")?.getJSONObject(0)?.getJSONObject(\"message\")?.get(\"content\")?.toString()?:\"\"\n\n    if (content.isNotEmpty()) {\n        content = content.replace(\"```json\",\"\").replace(\"```\",\"\")\n        return content\n    } else {\n        throw MonicaException(\"无法获取调色的参数\")\n    }\n}\n\nfun sendPostJson(url: String, headers: Map<String, String>, body: Any): String {\n\n    return runBlocking {\n        val requestBody = RequestBody.create(\"application/json; charset=utf-8\".toMediaTypeOrNull(), body.toString())\n\n        httpClient.okHttpClient()\n            .asyncCall {\n                Request.Builder()\n                    .url(url)\n                    .headers(headers.toHeaders())\n                    .post(requestBody)\n                    .build()\n            }\n            .get().body?.string()?:\"\"\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/llm/DialogSession.kt",
    "content": "package cn.netdiscovery.monica.llm\n\nimport cn.netdiscovery.monica.domain.ColorCorrectionSettings\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.llm.DialogSession\n * @author: Tony Shen\n * @date: 2025/8/1 17:27\n * @version: V1.0 封装一个会话上下文类（保留系统提示 + 当前参数）\n */\ndata class DialogSession(\n    val systemPrompt: String,\n    var currentSettings: ColorCorrectionSettings,\n    val history: MutableList<ColorCorrectionHistoryItem> = mutableListOf(),\n    var lastUsedProvider: LLMProvider? = null // 记录上次使用的 LLM 提供商\n)\n\n/**\n * 调色历史记录项\n */\ndata class ColorCorrectionHistoryItem(\n    val userInstruction: String,\n    val resultSettings: ColorCorrectionSettings,\n    val usedProvider: LLMProvider\n)"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/llm/GeminiClient.kt",
    "content": "package cn.netdiscovery.monica.llm\n\nimport cn.netdiscovery.http.core.utils.extension.asyncCall\nimport cn.netdiscovery.monica.domain.ColorCorrectionSettings\nimport cn.netdiscovery.monica.exception.MonicaException\nimport cn.netdiscovery.monica.http.httpClient\nimport com.safframework.rxcache.utils.GsonUtils\nimport kotlinx.coroutines.runBlocking\nimport okhttp3.Headers.Companion.toHeaders\nimport okhttp3.MediaType.Companion.toMediaTypeOrNull\nimport okhttp3.Request\nimport okhttp3.RequestBody\nimport org.json.JSONObject\nimport org.slf4j.Logger\nimport org.slf4j.LoggerFactory\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.llm.GeminiClient\n * @author: Tony Shen\n * @date: 2025/9/4 16:30\n * @version: V1.0 <描述当前版本功能>\n */\nprivate val logger: Logger = LoggerFactory.getLogger(object : Any() {}.javaClass.enclosingClass)\n\n/**\n * 使用 Gemini API 进行自然语言调色\n */\n@Throws(MonicaException::class)\nfun applyInstructionWithGemini(\n    session: DialogSession,\n    instruction: String,\n    apiKey: String\n): ColorCorrectionSettings? {\n    val prompt = buildString {\n        append(\"当前图像的参数如下：\\n\")\n        append(GsonUtils.toJson(session.currentSettings))\n        append(\"\\n\\n\")\n        append(\"用户指令：$instruction\")\n    }\n    \n    val geminiRequest = GeminiRequest(\n        contents = listOf(\n            GeminiContent(\n                parts = listOf(\n                    GeminiPart(text = systemPromptForColorCorrection + \"\\n\\n\" + prompt)\n                )\n            )\n        )\n    )\n    \n    val payload = GsonUtils.toJson(geminiRequest)\n\n    val responseJson = sendPostJsonToGemini(\n        url = \"$GEMINI_URL$apiKey\",\n        headers = mapOf(\"Content-Type\" to \"application/json\"),\n        body = payload\n    )\n\n    try {\n        val json = extractJsonFromGemini(responseJson)\n        val responseObj = GsonUtils.fromJson<ColorCorrectionSettings>(json, ColorCorrectionSettings::class.java)\n\n        session.currentSettings = responseObj\n        // 历史记录现在在 LLMServiceManager 中处理\n        return responseObj\n    } catch (e: Exception) {\n        logger.error(\"responseJson = $responseJson\")\n        logger.error(e.message, e)\n        throw MonicaException(\"无法获取调色的参数\")\n    }\n}\n\nfun extractJsonFromGemini(jsonData: String): String {\n    val jsonObject: JSONObject = JSONObject(jsonData)\n    var content = jsonObject.getJSONArray(\"candidates\")?.getJSONObject(0)?.getJSONObject(\"content\")?.getJSONArray(\"parts\")?.getJSONObject(0)?.get(\"text\")?.toString()?:\"\"\n\n    if (content.isNotEmpty()) {\n        content = content.replace(\"```json\",\"\").replace(\"```\",\"\")\n        return content\n    } else {\n        throw MonicaException(\"无法获取调色的参数\")\n    }\n}\n\nfun sendPostJsonToGemini(url: String, headers: Map<String, String>, body: Any): String {\n    return runBlocking {\n        val requestBody = RequestBody.create(\"application/json; charset=utf-8\".toMediaTypeOrNull(), body.toString())\n\n        httpClient.okHttpClient()\n            .asyncCall {\n                Request.Builder()\n                    .url(url)\n                    .headers(headers.toHeaders())\n                    .post(requestBody)\n                    .build()\n            }\n            .get().body?.string()?:\"\"\n    }\n}\n"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/llm/GeminiRequest.kt",
    "content": "package cn.netdiscovery.monica.llm\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.llm.GeminiRequest\n * @author: Tony Shen\n * @date: 2025/9/4 16:30\n * @version: V1.0 <描述当前版本功能>\n */\n\n/**\n * Gemini API 请求数据类\n */\ndata class GeminiRequest(\n    val contents: List<GeminiContent>\n)\n\n/**\n * Gemini API 内容数据类\n */\ndata class GeminiContent(\n    val parts: List<GeminiPart>\n)\n\n/**\n * Gemini API 部分数据类\n */\ndata class GeminiPart(\n    val text: String\n)\n"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/llm/LLMServiceManager.kt",
    "content": "package cn.netdiscovery.monica.llm\n\nimport androidx.compose.runtime.*\nimport cn.netdiscovery.monica.domain.ColorCorrectionSettings\nimport cn.netdiscovery.monica.exception.MonicaException\nimport org.slf4j.Logger\nimport org.slf4j.LoggerFactory\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.llm.LLMServiceManager\n * @author: Tony Shen\n * @date: 2025/9/4 17:45\n * @version: V1.0 <描述当前版本功能>\n */\nprivate val logger: Logger = LoggerFactory.getLogger(object : Any() {}.javaClass.enclosingClass)\n\nval systemPromptForColorCorrection = \"你是一个图像调色助手。用户会输入一句话，你需要将这句话转换为一组 JSON 格式的调色参数，字段说明如下：\\\\n\\\\n- contrast: 对比度，整数，范围 0 - 510，默认值 255。\\\\n- hue: 色调，整数，范围 0 - 360，默认值 180。\\\\n- saturation: 饱和度，整数，范围 0 - 510，默认值 255。\\\\n- lightness: 亮度，整数，范围 0 - 510，默认值 255。\\\\n- temperature: 色温，整数，范围 0 - 510，默认值 255。\\\\n- highlight: 高光，整数，范围 0 - 510，默认值 255。\\\\n- shadow: 阴影，整数，范围 0 - 510，默认值 255。\\\\n- sharpen: 锐化，整数，范围 0 - 255，默认值 0。\\\\n- corner: 暗角，整数，范围 0 - 255，默认值 0。\\\\n- status: 表示用户意图主要修改了哪一项（用于前端高亮显示），值如下：\\\\n  - 1 表示 contrast\\\\n  - 2 表示 hue\\\\n  - 3 表示 saturation\\\\n  - 4 表示 lightness\\\\n  - 5 表示 temperature\\\\n  - 6 表示 highlight\\\\n  - 7 表示 shadow\\\\n  - 8 表示 sharpen\\\\n  - 9 表示 corner\\\\n\\\\n要求：\\\\n- 不要输出解释。\\\\n- 严格输出 JSON 格式，字段顺序与上方一致。\\\\n- 如果用户输入不涉及某些参数，请保留默认值。\\\\n- 请根据语义合理推测用户意图。\".trimIndent()\n\nval DEEPSEEK_URL = \"https://api.deepseek.com/chat/completions\"\n\nval GEMINI_URL = \"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=\"\n\n/**\n * 记住 LLM 服务管理器实例\n */\n@Composable\nfun rememberLLMServiceManager(): LLMServiceManager {\n    return remember { LLMServiceManager() }\n}\n\n/**\n * LLM 服务提供商枚举\n */\nenum class LLMProvider {\n    DEEPSEEK,\n    GEMINI\n}\n\n\n/**\n * LLM 服务管理器\n * 统一管理不同的 LLM 服务提供商\n */\nclass LLMServiceManager {\n    \n    /**\n     * 使用指定的 LLM 服务提供商进行自然语言调色\n     * \n     * @param provider LLM 服务提供商\n     * @param session 对话会话\n     * @param instruction 用户指令\n     * @param apiKey API 密钥\n     * @return 更新后的颜色校正设置\n     * @throws MonicaException 当调用失败时抛出异常\n     */\n    fun applyInstructionWithLLM(\n        provider: LLMProvider,\n        session: DialogSession,\n        instruction: String,\n        apiKey: String\n    ): ColorCorrectionSettings? {\n        val result = when (provider) {\n            LLMProvider.DEEPSEEK -> {\n                logger.info(\"使用 DeepSeek 进行自然语言调色\")\n                applyInstructionWithLLM(session, instruction, apiKey)\n            }\n            LLMProvider.GEMINI -> {\n                logger.info(\"使用 Gemini 进行自然语言调色\")\n                applyInstructionWithGemini(session, instruction, apiKey)\n            }\n        }\n        \n        // 如果调用成功，记录历史记录\n        result?.let { settings ->\n            val historyItem = ColorCorrectionHistoryItem(\n                userInstruction = instruction,\n                resultSettings = settings,\n                usedProvider = provider\n            )\n            session.history.add(historyItem)\n            logger.info(\"记录调色历史: 使用 ${provider.name} 处理指令 '$instruction'\")\n        }\n        \n        return result\n    }\n}\n"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/manager/OpenCVManager.kt",
    "content": "package cn.netdiscovery.monica.manager\n\nimport cn.netdiscovery.monica.imageprocess.BufferedImages\nimport cn.netdiscovery.monica.imageprocess.utils.extension.toImageInfo\nimport cn.netdiscovery.monica.state.ApplicationState\nimport cn.netdiscovery.monica.utils.CVAction\nimport cn.netdiscovery.monica.utils.CVFailure\nimport cn.netdiscovery.monica.utils.CVSuccess\nimport kotlinx.coroutines.suspendCancellableCoroutine\nimport java.awt.image.BufferedImage\nimport kotlin.coroutines.resume\nimport kotlin.coroutines.resumeWithException\n\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.manager.OpenCVManager\n * @author: Tony Shen\n * @date: 2024/8/13 19:54\n * @version: V1.0\n */\nobject OpenCVManager {\n\n    /**\n     * 封装调用 OpenCV 的方法\n     * 便于\"当前的图像\"进行调用 OpenCV 的方法，以及对返回的 IntArray 进行处理返回成 BufferedImage\n     *\n     * @param state   当前应用的 state\n     * @param type    生成图像的类型\n     * @param action  通过 jni 调用 OpenCV 的方法\n     * @param failure 失败的回调\n     */\n    fun invokeCV(state: ApplicationState,\n                 type:Int = BufferedImage.TYPE_INT_ARGB,\n                 action: CVAction,\n                 failure: CVFailure) {\n\n        if (state.currentImage!=null) {\n            val (width,height,byteArray) = state.currentImage!!.toImageInfo()\n\n            try {\n                val outPixels = action.invoke(byteArray)\n                state.addQueue(state.currentImage!!)\n                state.currentImage = BufferedImages.toBufferedImage(outPixels,width,height,type)\n            } catch (e:Exception) {\n                failure.invoke(e)\n            }\n        }\n    }\n\n    /**\n     * 封装调用 OpenCV 的方法\n     * 便于对某个图像调用 OpenCV 的方法，以及对返回的 IntArray 进行处理返回成 BufferedImage\n     *\n     * @param image   对该图片进行处理\n     * @param type    生成图像的类型\n     * @param action  通过 jni 调用 OpenCV 的方法\n     * @param success 成功的回调\n     * @param failure 失败的回调\n     */\n    fun invokeCV(image: BufferedImage,\n                 type:Int = BufferedImage.TYPE_INT_ARGB,\n                 action: CVAction,\n                 success: CVSuccess,\n                 failure: CVFailure) {\n        val (width,height,byteArray) = image.toImageInfo()\n\n        try {\n            val outPixels = action.invoke(byteArray)\n            success.invoke(BufferedImages.toBufferedImage(outPixels,width,height,type))\n        } catch (e:Exception) {\n            failure.invoke(e)\n        }\n    }\n\n    suspend fun invokeCVSuspend(\n        image: BufferedImage,\n        type: Int = BufferedImage.TYPE_INT_ARGB,\n        action: CVAction\n    ): BufferedImage = suspendCancellableCoroutine { cont ->\n        invokeCV(\n            image = image,\n            type = type,\n            action = action,\n            success = { result ->\n                if (cont.isActive) cont.resume(result)\n            },\n            failure = { e ->\n                if (cont.isActive) cont.resumeWithException(e)\n            }\n        )\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/state/ApplicationState.kt",
    "content": "package cn.netdiscovery.monica.state\n\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.awt.ComposeWindow\nimport androidx.compose.ui.window.Notification\nimport androidx.compose.ui.window.TrayState\nimport cn.netdiscovery.monica.config.KEY_GENERAL_SETTINGS\nimport cn.netdiscovery.monica.config.category.ConfigCategoryManager\nimport cn.netdiscovery.monica.domain.DecodedPreviewImage\nimport cn.netdiscovery.monica.i18n.LocalizationManager\nimport cn.netdiscovery.monica.domain.GeneralSettings\nimport cn.netdiscovery.monica.opencv.ImageProcess\nimport cn.netdiscovery.monica.ui.theme.ColorTheme\nimport cn.netdiscovery.monica.ui.theme.ThemeManager\nimport cn.netdiscovery.monica.utils.ImageFormat\nimport kotlinx.coroutines.CoroutineScope\nimport java.awt.image.BufferedImage\nimport java.io.File\nimport java.util.concurrent.LinkedBlockingDeque\nimport java.util.concurrent.TimeUnit\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.state.ApplicationState\n * @author: Tony Shen\n * @date: 2024/4/26 10:42\n * @version: V1.0 <描述当前版本功能>\n */\nconst val ZoomPreviewStatus: Int = 1\nconst val BlurStatus: Int = 2\nconst val MosaicStatus: Int = 3\nconst val DoodleStatus: Int = 4\nconst val ShapeDrawingStatus: Int = 5\nconst val ColorPickStatus: Int = 6\nconst val GenerateGifStatus: Int = 7\nconst val FlipStatus: Int = 8\nconst val RotateStatus: Int = 9\nconst val ResizeStatus: Int = 10\nconst val ShearingStatus: Int = 11\nconst val CropSizeStatus: Int = 12\nconst val CompressionStatus: Int = 13\n\nconst val ColorCorrectionStatus: Int = 14\nconst val FilterStatus: Int = 15\n\n\nconst val OpenCVDebugStatus: Int = 16\nconst val FaceDetectStatus: Int = 17\nconst val SketchDrawingStatus: Int = 18\nconst val FaceSwapStatus: Int = 19\nconst val CartoonStatus: Int = 20\nconst val WebScreenshotStatus: Int = 21\n\n\n\n@Composable\nfun rememberApplicationState(\n    scope: CoroutineScope,\n    trayState: TrayState\n) = remember {\n    ApplicationState(scope, trayState)\n}\n\nclass ApplicationState(val scope:CoroutineScope,\n                       val trayState: TrayState) {\n\n    lateinit var window: ComposeWindow\n\n    var rawImage: BufferedImage? by mutableStateOf(null)\n    var currentImage: BufferedImage? by mutableStateOf( rawImage )\n    var rawImageFile: File? = null\n    var rawImageFormat: ImageFormat? = null\n    var nativeImageInfo: DecodedPreviewImage? = null\n    var nativeFullImageProcessed: Boolean = false\n\n    // 表示用于点击了哪个功能\n    var currentStatus by mutableStateOf(0)\n\n    var isGeneralSettings by mutableStateOf(false)\n    var isBasic by mutableStateOf(false)\n    var isColorCorrection by mutableStateOf(false)\n    var isFilter by mutableStateOf(false)\n    var isAI by mutableStateOf(false)\n    var isCompression by mutableStateOf(false)\n\n    var isShowPreviewWindow by mutableStateOf(false)\n\n    private val queue: LinkedBlockingDeque<BufferedImage> = LinkedBlockingDeque(40)\n\n    // 通用输出框的颜色\n    private val defaultSettings = GeneralSettings(255, 255, 255, 512, 50, \"\", \"\", \"\", \"LIGHT\")\n    \n    private fun loadGeneralSettings(): GeneralSettings {\n        return ConfigCategoryManager.load(KEY_GENERAL_SETTINGS, defaultSettings)\n    }\n    \n    private val initialSettings = loadGeneralSettings()\n    \n    var outputBoxRText by mutableStateOf(initialSettings.outputBoxR)\n    var outputBoxGText by mutableStateOf(initialSettings.outputBoxG)\n    var outputBoxBText by mutableStateOf(initialSettings.outputBoxB)\n\n    var sizeText by mutableStateOf(initialSettings.size)\n    var maxHistorySizeText by mutableStateOf(initialSettings.maxHistorySize)\n    var deepSeekApiKeyText by mutableStateOf(initialSettings.deepSeekApiKey)\n    var geminiApiKeyText by mutableStateOf(initialSettings.geminiApiKey)\n    var algorithmUrlText by mutableStateOf(initialSettings.algorithmUrl)\n\n    // 主题设置 - 作为唯一的状态源\n    var currentTheme by mutableStateOf(\n        initialSettings.themeId.let { themeId ->\n            ThemeManager.getThemeById(themeId) ?: ColorTheme.LIGHT\n        }\n    )\n    \n    // 初始化时同步到ThemeManager\n    init {\n        ThemeManager.setCurrentTheme(currentTheme)\n    }\n\n    fun toOutputBoxScalar() = intArrayOf(outputBoxBText, outputBoxGText, outputBoxRText)\n\n    fun saveGeneralSettings() {\n        val settings = GeneralSettings(\n            outputBoxRText, outputBoxGText, outputBoxBText, \n            sizeText, maxHistorySizeText, \n            deepSeekApiKeyText, geminiApiKeyText, algorithmUrlText, \n            currentTheme.getThemeId()\n        )\n        ConfigCategoryManager.save(KEY_GENERAL_SETTINGS, settings)\n    }\n\n    /**\n     * 切换主题 - 确保状态同步\n     */\n    fun setTheme(theme: ColorTheme) {\n        currentTheme = theme\n        ThemeManager.setCurrentTheme(theme)\n        // 立即保存到缓存\n        saveGeneralSettings()\n    }\n\n    /**\n     * 获取当前主题\n     */\n    fun getCurrentThemeValue(): ColorTheme = currentTheme\n\n    fun getLastImage():BufferedImage? = queue.pollFirst(1, TimeUnit.SECONDS)\n\n    fun addQueue(bufferedImage: BufferedImage) {\n        queue.putFirst(bufferedImage)\n    }\n\n    fun clearQueue() {\n        queue.clear()\n    }\n\n    fun togglePreviewWindow(isShow: Boolean = true) {\n        isShowPreviewWindow = isShow\n    }\n\n    /**\n     * 弹出新的页面，更新 currentStatus 状态\n     * @param status 更新为当前的状态\n     */\n    fun togglePreviewWindowAndUpdateStatus(status:Int) {\n        currentStatus = status\n        isShowPreviewWindow = true\n    }\n\n    /**\n     * 关闭当前弹出的页面\n     */\n    fun closePreviewWindow() {\n        resetCurrentStatus()\n        togglePreviewWindow(false)\n    }\n\n    /**\n     * 清空了当前的状态\n     */\n    fun resetCurrentStatus() {\n        currentStatus = 0\n    }\n\n    fun clearImage() {\n        this.rawImage = null\n        this.currentImage = null\n        this.rawImageFile = null\n        this.rawImageFormat = null\n        this.nativeFullImageProcessed = false\n\n        val nativePtr = this.nativeImageInfo?.nativePtr\n        if (nativePtr!=null && nativePtr !=0L) {\n            ImageProcess.deletePyramidImage(nativePtr)\n        }\n        this.nativeImageInfo = null\n    }\n\n    fun showTray(\n        msg: String,\n        title: String = LocalizationManager.getString(\"notification\"),\n        type: Notification.Type = Notification.Type.Info\n    ) {\n        val notification = Notification(title, msg, type)\n        trayState.sendNotification(notification)\n    }\n}\n"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/BasicView.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel\n\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.unit.dp\nimport cn.netdiscovery.monica.exception.ErrorSeverity\nimport cn.netdiscovery.monica.exception.ErrorType\nimport cn.netdiscovery.monica.exception.showError\nimport cn.netdiscovery.monica.ui.i18n.rememberI18nState\nimport cn.netdiscovery.monica.state.*\nimport cn.netdiscovery.monica.ui.preview.PreviewViewModel\nimport cn.netdiscovery.monica.ui.widget.*\nimport cn.netdiscovery.monica.utils.getValidateField\nimport org.koin.compose.koinInject\nimport org.slf4j.Logger\nimport org.slf4j.LoggerFactory\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.BasicView\n * @author: Tony Shen\n * @date: 2024/5/1 00:39\n * @version: V1.0 <描述当前版本功能>\n */\nprivate val logger: Logger = LoggerFactory.getLogger(object : Any() {}.javaClass.enclosingClass)\n\n@Composable\nfun basicView(state: ApplicationState) {\n    val i18nState = rememberI18nState()\n    val viewModel: PreviewViewModel = koinInject()\n\n    Row(verticalAlignment = Alignment.CenterVertically) {\n\n        // 图像模糊\n        toolTipButton(\n            text = i18nState.getString(\"image_blur\"),\n            painter = painterResource(\"images/controlpanel/blur.png\"),\n            enable = { state.isBasic },\n            onClick = {\n                state.currentStatus = BlurStatus\n            })\n\n        // 图像马赛克\n        toolTipButton(\n            text = i18nState.getString(\"image_mosaic\"),\n            painter = painterResource(\"images/controlpanel/mosaic.png\"),\n            enable = { state.isBasic },\n            onClick = {\n                state.currentStatus = MosaicStatus\n            })\n\n        // 图像涂鸦\n        toolTipButton(\n            text = i18nState.getString(\"image_doodle\"),\n            painter = painterResource(\"images/controlpanel/doodle.png\"),\n            enable = { state.isBasic },\n            onClick = {\n                state.togglePreviewWindowAndUpdateStatus(DoodleStatus)\n            })\n\n        // 形状绘制\n        toolTipButton(\n            text = i18nState.getString(\"shape_drawing\"),\n            painter = painterResource(\"images/controlpanel/shape-drawing.png\"),\n            enable = { state.isBasic },\n            onClick = {\n                state.togglePreviewWindowAndUpdateStatus(ShapeDrawingStatus)\n            })\n    }\n\n    Row(verticalAlignment = Alignment.CenterVertically) {\n        // 图像取色\n        toolTipButton(text = i18nState.getString(\"color_picker\"),\n            painter = painterResource(\"images/controlpanel/color-picker.png\"),\n            enable = { state.isBasic },\n            onClick = {\n                state.togglePreviewWindowAndUpdateStatus(ColorPickStatus)\n            })\n\n        // 生成gif\n        toolTipButton(text = i18nState.getString(\"generate_gif\"),\n            painter = painterResource(\"images/controlpanel/gif.png\"),\n            enable = { state.isBasic },\n            onClick = {\n                state.togglePreviewWindowAndUpdateStatus(GenerateGifStatus)\n            })\n\n        // 图像翻转\n        toolTipButton(text = i18nState.getString(\"image_flip\"),\n            painter = painterResource(\"images/controlpanel/flip.png\"),\n            enable = { state.isBasic },\n            onClick = {\n                state.currentStatus = FlipStatus\n                viewModel.flip(state)\n            })\n\n        // 图像旋转\n        toolTipButton(text = i18nState.getString(\"image_rotate\"),\n            painter = painterResource(\"images/controlpanel/rotate.png\"),\n            enable = { state.isBasic },\n            onClick = {\n                state.currentStatus = RotateStatus\n                viewModel.rotate(state)\n            })\n    }\n\n    Row(verticalAlignment = Alignment.CenterVertically) {\n        // 图像缩放\n        toolTipButton(text = i18nState.getString(\"image_scale\"),\n            painter = painterResource(\"images/controlpanel/resize.png\"),\n            enable = { state.isBasic },\n            onClick = {\n                state.currentStatus = ResizeStatus\n            })\n\n        // 图像错切\n        toolTipButton(text = i18nState.getString(\"image_shear\"),\n            painter = painterResource(\"images/controlpanel/shearing.png\"),\n            enable = { state.isBasic },\n            onClick = {\n                state.currentStatus = ShearingStatus\n            })\n\n        // 图像裁剪\n        toolTipButton(text = i18nState.getString(\"image_crop\"),\n            painter = painterResource(\"images/controlpanel/crop.png\"),\n            enable = { state.isBasic },\n            onClick = {\n                state.togglePreviewWindowAndUpdateStatus(CropSizeStatus)\n            })\n\n        // 图像压缩\n        toolTipButton(text = i18nState.getString(\"image_compression\"),\n            painter = painterResource(\"images/controlpanel/compress.png\"),\n            enable = { state.isBasic },\n            onClick = {\n                state.togglePreviewWindowAndUpdateStatus(CompressionStatus)\n            })\n    }\n\n    Column {\n        when(state.currentStatus) {\n            ResizeStatus   -> generateResizeParams(state,viewModel)\n\n            ShearingStatus -> generateShearingParams(state,viewModel)\n        }\n    }\n}\n\n\n@Composable\nprivate fun generateResizeParams(state: ApplicationState, viewModel: PreviewViewModel) {\n    val i18nState = rememberI18nState()\n\n    var widthText by remember {\n        mutableStateOf(\"${state.currentImage?.width?:400}\")\n    }\n\n    var heightText by remember {\n        mutableStateOf(\"${state.currentImage?.height?:400}\")\n    }\n\n    Column {\n        basicTextFieldWithTitle(titleText = \"width\", widthText, Modifier.padding(top = 5.dp)) { str ->\n            widthText = str\n        }\n\n        basicTextFieldWithTitle(titleText = \"height\", heightText, Modifier.padding(top = 5.dp)) { str ->\n            heightText = str\n        }\n    }\n\n    Row(\n        modifier = Modifier.fillMaxWidth(),\n        horizontalArrangement = Arrangement.End,\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        confirmButton(state.isBasic) {\n\n            val width = getValidateField(block = { widthText.toInt() } , failed = {\n                val errorMsg = i18nState.getString(\"width_needs_int\")\n                showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.MEDIUM, errorMsg, errorMsg)\n            }) ?: return@confirmButton\n\n            val height = getValidateField(block = { heightText.toInt() } , failed = {\n                val errorMsg = i18nState.getString(\"height_needs_int\")\n                showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.MEDIUM, errorMsg, errorMsg)\n            }) ?: return@confirmButton\n\n            viewModel.resize(width, height, state)\n        }\n    }\n}\n\n\n@Composable\nprivate fun generateShearingParams(state: ApplicationState, viewModel: PreviewViewModel) {\n    val i18nState = rememberI18nState()\n\n    var xText by remember {\n        mutableStateOf(\"${0}\")\n    }\n\n    var yText by remember {\n        mutableStateOf(\"${0}\")\n    }\n\n    Column {\n        basicTextFieldWithTitle(titleText = i18nState.getString(\"x_direction\"), xText, Modifier.padding(top = 5.dp)) { str ->\n            xText = str\n        }\n\n        basicTextFieldWithTitle(titleText = i18nState.getString(\"y_direction\"), yText, Modifier.padding(top = 5.dp)) { str ->\n            yText = str\n        }\n    }\n\n    Row(\n        modifier = Modifier.fillMaxWidth(),\n        horizontalArrangement = Arrangement.End,\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        confirmButton(state.isBasic) {\n\n            val x = getValidateField(block = { xText.toFloat() } , failed = {\n                val errorMsg = i18nState.getString(\"x_direction_needs_float\")\n                showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.MEDIUM, errorMsg, errorMsg)\n            }) ?: return@confirmButton\n            val y = getValidateField(block = { yText.toFloat() } , failed = {\n\n                val errorMsg = i18nState.getString(\"y_direction_needs_float\")\n                showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.MEDIUM, errorMsg, errorMsg)\n            }) ?: return@confirmButton\n            viewModel.shearing(x, y, state)\n        }\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/ai/AIView.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.ai\n\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.material.Checkbox\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.res.painterResource\nimport cn.netdiscovery.monica.ui.i18n.rememberI18nState\nimport cn.netdiscovery.monica.state.*\nimport cn.netdiscovery.monica.ui.widget.subTitle\nimport cn.netdiscovery.monica.ui.widget.toolTipButton\nimport org.koin.compose.koinInject\nimport org.slf4j.Logger\nimport org.slf4j.LoggerFactory\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.ai.AIView\n * @author: Tony Shen\n * @date: 2024/7/27 11:13\n * @version: V1.0 <描述当前版本功能>\n */\nprivate val logger: Logger = LoggerFactory.getLogger(object : Any() {}.javaClass.enclosingClass)\n\n@Composable\nfun aiView(state: ApplicationState) {\n    val i18nState = rememberI18nState()\n    val viewModel: AIViewModel = koinInject()\n\n    Row (\n        verticalAlignment = Alignment.CenterVertically\n    ) {\n        toolTipButton(\n            text = i18nState.getString(\"simple_cv_algorithm\"),\n            painter = painterResource(\"images/ai/experiment.png\"),\n            enable = { state.isAI },\n            onClick = {\n                state.togglePreviewWindowAndUpdateStatus(OpenCVDebugStatus)\n            })\n\n        toolTipButton(\n            text = i18nState.getString(\"face_detection\"),\n            painter = painterResource(\"images/ai/face_detect.png\"),\n            enable = { state.isAI },\n            onClick = {\n                state.currentStatus = FaceDetectStatus\n                viewModel.faceDetect(state)\n            })\n\n        toolTipButton(\n            text = i18nState.getString(\"generate_sketch\"),\n            painter = painterResource(\"images/ai/sketch_drawing.png\"),\n            enable = { state.isAI },\n            onClick = {\n                state.currentStatus = SketchDrawingStatus\n                viewModel.sketchDrawing(state)\n            })\n\n        toolTipButton(text = i18nState.getString(\"face_swap\"),\n            painter = painterResource(\"images/ai/face_swap.png\"),\n            enable = { state.isAI },\n            onClick = {\n                state.togglePreviewWindowAndUpdateStatus(FaceSwapStatus)\n            })\n\n    }\n\n    Row (\n        verticalAlignment = Alignment.CenterVertically\n    ) {\n        toolTipButton(text = i18nState.getString(\"anime_style\"),\n            painter = painterResource(\"images/ai/cartoon.png\"),\n            enable = { state.isAI },\n            onClick = {\n                state.togglePreviewWindowAndUpdateStatus(CartoonStatus)\n            })\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/ai/AIViewModel.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.ai\n\nimport cn.netdiscovery.monica.exception.ErrorSeverity\nimport cn.netdiscovery.monica.exception.ErrorType\nimport cn.netdiscovery.monica.exception.showError\nimport cn.netdiscovery.monica.http.createRequest\nimport cn.netdiscovery.monica.http.createRequestBody\nimport cn.netdiscovery.monica.state.ApplicationState\nimport cn.netdiscovery.monica.utils.ImageFormatDetector\nimport cn.netdiscovery.monica.utils.extensions.launchWithSuspendLoading\nimport cn.netdiscovery.monica.utils.logger\nimport okhttp3.Request\nimport okhttp3.RequestBody\nimport org.slf4j.Logger\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.ai.AIViewModel\n * @author: Tony Shen\n * @date: 2024/7/28 11:21\n * @version: V1.0 <描述当前版本功能>\n */\nclass AIViewModel {\n    private val logger: Logger = logger<AIViewModel>()\n\n    fun faceDetect(state: ApplicationState) {\n        if (state.currentImage == null) return\n\n        state.scope.launchWithSuspendLoading {\n\n            createRequest(request = {\n                val format = ImageFormatDetector.getImageFormat(state.rawImageFile!!)?:\"jpg\"\n\n                val requestBody: RequestBody = createRequestBody(state.currentImage!!,format)\n\n                Request.Builder()\n                    .url( \"${state.algorithmUrlText}api/faceDetect\")\n                    .post(requestBody)\n                    .build()\n            }, success = {\n                state.addQueue(state.currentImage!!)\n                state.currentImage = it\n            }, failure = {\n                logger.error(it.message)\n\n                showError(ErrorType.AI_SERVICE_ERROR, ErrorSeverity.MEDIUM, \"算法服务异常\", \"算法服务异常\")\n            })\n        }\n    }\n\n    fun sketchDrawing(state: ApplicationState) {\n        if (state.currentImage == null) return\n\n        state.scope.launchWithSuspendLoading {\n\n            createRequest(request = {\n                val format = ImageFormatDetector.getImageFormat(state.rawImageFile!!)?:\"jpg\"\n\n                val requestBody: RequestBody = createRequestBody(state.currentImage!!,format)\n\n                Request.Builder()\n                    .url( \"${state.algorithmUrlText}api/sketchDrawing\")\n                    .post(requestBody)\n                    .build()\n            }, success = {\n                state.addQueue(state.currentImage!!)\n                state.currentImage = it\n            }, failure = {\n                logger.error(it.message)\n\n                showError(ErrorType.AI_SERVICE_ERROR, ErrorSeverity.MEDIUM, \"算法服务异常\", \"算法服务异常\")\n            })\n        }\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/ai/experiment/BinaryImageView.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.ai.experiment\n\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material.Button\nimport androidx.compose.material.RadioButton\nimport androidx.compose.material.Text\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.unit.dp\nimport cn.netdiscovery.monica.state.ApplicationState\nimport cn.netdiscovery.monica.ui.controlpanel.ai.experiment.viewmodel.BinaryImageViewModel\nimport cn.netdiscovery.monica.ui.i18n.rememberI18nState\nimport cn.netdiscovery.monica.ui.controlpanel.ai.experiment.viewmodel.EdgeDetectionViewModel\nimport cn.netdiscovery.monica.ui.widget.*\nimport cn.netdiscovery.monica.utils.getValidateField\nimport cn.netdiscovery.monica.exception.showError\nimport cn.netdiscovery.monica.exception.ErrorType\nimport cn.netdiscovery.monica.exception.ErrorSeverity\nimport org.koin.compose.koinInject\nimport org.slf4j.Logger\nimport org.slf4j.LoggerFactory\nimport java.awt.image.BufferedImage\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.ai.experiment.BinaryImageAnalysisView\n * @author: Tony Shen\n * @date:  2024/10/2 15:03\n * @version: V1.0 <描述当前版本功能>\n */\nprivate val logger: Logger = LoggerFactory.getLogger(object : Any() {}.javaClass.enclosingClass)\n\nval typeSelectTags = arrayListOf(\"THRESH_BINARY\", \"THRESH_BINARY_INV\")\nval thresholdSelectTags = arrayListOf(\"THRESH_OTSU\", \"THRESH_TRIANGLE\")\nval adaptiveMethodSelectTags = arrayListOf(\"ADAPTIVE_THRESH_MEAN_C\", \"ADAPTIVE_THRESH_GAUSSIAN_C\")\n\n@Composable\nfun binaryImage(state: ApplicationState, title: String) {\n    val i18nState = rememberI18nState()\n    val viewModel: BinaryImageViewModel = koinInject()\n    val edgeDetectionViewModel: EdgeDetectionViewModel = koinInject()\n\n    var typeSelectedOption by remember { mutableStateOf(\"Null\") }\n    var thresholdSelectedOption by remember { mutableStateOf(\"Null\") }\n    var adaptiveMethodSelectedOption by remember { mutableStateOf(\"Null\") }\n\n    var blockSizeText by remember { mutableStateOf(\"\") }\n    var cText by remember { mutableStateOf(\"\") }\n\n    var threshold1Text by remember { mutableStateOf(\"\") }\n    var threshold2Text by remember { mutableStateOf(\"\") }\n    var apertureSizeText by remember { mutableStateOf(\"3\") }\n\n    var hminText by remember { mutableStateOf(\"\") }\n    var sminText by remember { mutableStateOf(\"\") }\n    var vminText by remember { mutableStateOf(\"\") }\n    var hmaxText by remember { mutableStateOf(\"\") }\n    var smaxText by remember { mutableStateOf(\"\") }\n    var vmaxText by remember { mutableStateOf(\"\") }\n\n    fun clearAdaptiveThreshParams() {\n        blockSizeText = \"\"\n        cText = \"\"\n    }\n\n    Column (modifier = Modifier.fillMaxSize().padding(start = 20.dp, end =  20.dp, top = 10.dp)) {\n        title(modifier = Modifier.align(Alignment.CenterHorizontally) , text = title, color = Color.Black)\n\n        Column {\n            subTitleWithDivider(text = i18nState.getString(\"grayscale_image\"), color = Color.Black)\n\n            Button(\n                modifier = Modifier.align(Alignment.End),\n                onClick = experimentViewClick(state) {\n\n                    if(state.currentImage!= null && state.currentImage?.type != BufferedImage.TYPE_BYTE_GRAY) {\n                        viewModel.cvtGray(state)\n                    }\n                }\n            ) {\n                Text(text = i18nState.getString(\"image_grayscale\"), color = Color.Unspecified)\n            }\n        }\n\n        Column(modifier = Modifier.padding(top = 20.dp)) {\n            subTitleWithDivider(text = i18nState.getString(\"threshold_segmentation\"), color = Color.Black)\n\n            checkBoxWithTitle(i18nState.getString(\"threshold_type\"), checked = CVState.isThreshType, onCheckedChange = {\n                CVState.isThreshType = it\n\n                if (!CVState.isThreshType) {\n                    typeSelectedOption = \"Null\"\n                    logger.info(\"取消了阈值化类型\")\n                } else {\n                    logger.info(\"勾选了阈值化类型\")\n                }\n            })\n\n            Row {\n                typeSelectTags.forEach {\n                    RadioButton(\n                        selected = (CVState.isThreshType && it == typeSelectedOption),\n                        onClick = {\n                            typeSelectedOption = it\n                        }\n                    )\n                    Text(text = it, modifier = Modifier.align(Alignment.CenterVertically))\n                }\n            }\n\n            checkBoxWithTitle(i18nState.getString(\"global_threshold_segmentation\"), modifier = Modifier.padding(top = 10.dp), checked = CVState.isThreshSegment, onCheckedChange = {\n                CVState.isThreshSegment = it\n\n                if (!CVState.isThreshSegment) {\n                    thresholdSelectedOption = \"Null\"\n                    logger.info(\"取消了全局阈值分割\")\n                } else {\n                    CVState.isAdaptiveThresh = false\n                    adaptiveMethodSelectedOption = \"Null\"\n                    clearAdaptiveThreshParams()\n                    logger.info(\"勾选了全局阈值分割\")\n                }\n            })\n\n            Row {\n                thresholdSelectTags.forEach {\n                    RadioButton(\n                        selected = (CVState.isThreshSegment && it == thresholdSelectedOption),\n                        onClick = {\n                            thresholdSelectedOption = it\n                        }\n                    )\n                    Text(text = it, modifier = Modifier.align(Alignment.CenterVertically))\n                }\n            }\n\n            checkBoxWithTitle(i18nState.getString(\"adaptive_threshold_segmentation\"), modifier = Modifier.padding(top = 10.dp), checked = CVState.isAdaptiveThresh, onCheckedChange = {\n                CVState.isAdaptiveThresh = it\n\n                if (!CVState.isAdaptiveThresh) {\n                    adaptiveMethodSelectedOption = \"Null\"\n                    clearAdaptiveThreshParams()\n                    logger.info(\"取消了自适应阈值分割\")\n                } else {\n                    CVState.isThreshSegment = false\n                    thresholdSelectedOption = \"Null\"\n                    logger.info(\"勾选了自适应阈值分割\")\n                }\n            })\n\n            Row {\n                Text(i18nState.getString(\"adaptive_threshold_algorithm\"), modifier = Modifier.align(Alignment.CenterVertically))\n\n                adaptiveMethodSelectTags.forEach {\n                    RadioButton(\n                        selected = (CVState.isAdaptiveThresh && it == adaptiveMethodSelectedOption),\n                        onClick = {\n                            adaptiveMethodSelectedOption = it\n                        }\n                    )\n                    Text(text = it, modifier = Modifier.align(Alignment.CenterVertically))\n                }\n            }\n\n            Row {\n                basicTextFieldWithTitle(titleText = \"blockSize\", blockSizeText) { str ->\n                    if (CVState.isAdaptiveThresh) {\n                        blockSizeText = str\n                    }\n                }\n\n                basicTextFieldWithTitle(titleText = \"c\", cText) { str ->\n                    if (CVState.isAdaptiveThresh) {\n                        cText = str\n                    }\n                }\n            }\n\n            Button(\n                modifier = Modifier.padding(top = 10.dp).align(Alignment.End),\n                onClick = experimentViewClick(state) {\n                    if(state.currentImage?.type != BufferedImage.TYPE_BYTE_BINARY) {\n\n                        if (CVState.isThreshType && CVState.isThreshSegment) {\n\n                            if (typeSelectedOption == \"Null\") {\n                                val errorMsg = i18nState.getString(\"please_select_threshold_type\")\n                                showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.MEDIUM, errorMsg, errorMsg)\n                                return@experimentViewClick\n                            }\n\n                            if (thresholdSelectedOption == \"Null\") {\n                                val errorMsg = i18nState.getString(\"please_select_global_threshold_segmentation\")\n                                showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.MEDIUM, errorMsg, errorMsg)\n                                return@experimentViewClick\n                            }\n\n                            viewModel.threshold(state, typeSelectedOption, thresholdSelectedOption)\n                        } else if (CVState.isThreshType && CVState.isAdaptiveThresh) {\n                            if (typeSelectedOption == \"Null\") {\n                                val errorMsg = i18nState.getString(\"please_select_threshold_type\")\n                                showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.MEDIUM, errorMsg, errorMsg)\n                                return@experimentViewClick\n                            }\n\n                            if (adaptiveMethodSelectedOption == \"Null\") {\n                                val errorMsg = i18nState.getString(\"please_select_adaptive_threshold_algorithm\")\n                                showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.MEDIUM, errorMsg, errorMsg)\n                                return@experimentViewClick\n                            }\n\n                            val blockSize = getValidateField(block = { blockSizeText.toInt() } , failed = { \n                                val errorMsg = i18nState.getString(\"block_size_needs_int\")\n                                showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.MEDIUM, errorMsg, errorMsg)\n                            })?: return@experimentViewClick\n\n                            val c = getValidateField(block = { cText.toInt() } , failed = { \n                                val errorMsg = i18nState.getString(\"c_needs_int\")\n                                showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.MEDIUM, errorMsg, errorMsg)\n                            })?: return@experimentViewClick\n\n                            viewModel.adaptiveThreshold(state, adaptiveMethodSelectedOption, typeSelectedOption, blockSize, c)\n                        } else {\n                            val errorMsg = i18nState.getString(\"please_select_threshold_type_and_segmentation\")\n                            showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.MEDIUM, errorMsg, errorMsg)\n                        }\n                    }\n                }\n            ) {\n                Text(text = i18nState.getString(\"threshold_segmentation\"), color = Color.Unspecified)\n            }\n        }\n\n        Column(modifier = Modifier.padding(top = 20.dp)) {\n            subTitleWithDivider(text = i18nState.getString(\"canny_edge_detection\"), color = Color.Black)\n\n            Row(modifier = Modifier.padding(top = 10.dp)){\n                basicTextFieldWithTitle(titleText = \"threshold1\", threshold1Text) { str ->\n                    threshold1Text = str\n                }\n\n                basicTextFieldWithTitle(titleText = \"threshold2\", threshold2Text) { str ->\n                    threshold2Text = str\n                }\n\n                basicTextFieldWithTitle(titleText = \"apertureSize\", apertureSizeText) { str ->\n                    apertureSizeText = str\n                }\n            }\n\n            Button(\n                modifier = Modifier.padding(top = 10.dp).align(Alignment.End),\n                onClick = experimentViewClick(state) {\n                    if(state.currentImage?.type != BufferedImage.TYPE_BYTE_BINARY) {\n                        val threshold1 = getValidateField(block = { threshold1Text.toDouble() } , failed = { \n                            val errorMsg = i18nState.getString(\"threshold1_needs_double\")\n                            showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.MEDIUM, errorMsg, errorMsg)\n                        })?: return@experimentViewClick\n                        val threshold2 = getValidateField(block = { threshold2Text.toDouble() } , failed = { \n                            val errorMsg = i18nState.getString(\"threshold2_needs_double\")\n                            showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.MEDIUM, errorMsg, errorMsg)\n                        })?: return@experimentViewClick\n                        val apertureSize = getValidateField(block = { apertureSizeText.toInt() } , failed = { \n                            val errorMsg = i18nState.getString(\"aperture_size_needs_int\")\n                            showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.MEDIUM, errorMsg, errorMsg)\n                        })?: return@experimentViewClick\n\n                        edgeDetectionViewModel.canny(state, threshold1, threshold2, apertureSize)\n                    }\n                }\n            ) {\n                Text(text = i18nState.getString(\"canny_edge_detection\"), color = Color.Unspecified)\n            }\n        }\n\n        Column(modifier = Modifier.padding(top = 20.dp)) {\n            subTitleWithDivider(text = i18nState.getString(\"color_image_segmentation\"), color = Color.Black)\n\n            Row(modifier = Modifier.padding(top = 10.dp)) {\n                basicTextFieldWithTitle(titleText = \"hmin\", hminText) { str ->\n                    hminText = str\n                }\n\n                basicTextFieldWithTitle(titleText = \"smin\", sminText) { str ->\n                    sminText = str\n                }\n\n                basicTextFieldWithTitle(titleText = \"vmin\", vminText) { str ->\n                    vminText = str\n                }\n            }\n\n            Row(modifier = Modifier.padding(top = 10.dp)){\n                basicTextFieldWithTitle(titleText = \"hmax\", hmaxText) { str ->\n                    hmaxText = str\n                }\n\n                basicTextFieldWithTitle(titleText = \"smax\", smaxText) { str ->\n                    smaxText = str\n                }\n\n                basicTextFieldWithTitle(titleText = \"vmax\", vmaxText) { str ->\n                    vmaxText = str\n                }\n            }\n\n            Button(\n                modifier = Modifier.padding(top = 10.dp).align(Alignment.End),\n                onClick = experimentViewClick(state) {\n                    if(state.currentImage?.type!! in 1..9) {\n                        val hmin = getValidateField(block = { hminText.toInt() }, failed = {\n                            val errorMsg = i18nState.getString(\"hmin_needs_int\")\n                            showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.MEDIUM, errorMsg, errorMsg)\n                        })?: return@experimentViewClick\n                        val smin = getValidateField(block = { sminText.toInt() } , failed = { \n                            val errorMsg = i18nState.getString(\"smin_needs_int\")\n                            showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.MEDIUM, errorMsg, errorMsg)\n                        })?: return@experimentViewClick\n                        val vmin = getValidateField(block = { vminText.toInt() } , failed = { \n                            val errorMsg = i18nState.getString(\"vmin_needs_int\")\n                            showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.MEDIUM, errorMsg, errorMsg)\n                        })?: return@experimentViewClick\n\n                        val hmax = getValidateField(block = { hmaxText.toInt() } , failed = { \n                            val errorMsg = i18nState.getString(\"hmax_needs_int\")\n                            showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.MEDIUM, errorMsg, errorMsg)\n                        })?: return@experimentViewClick\n                        val smax = getValidateField(block = { smaxText.toInt() } , failed = { \n                            val errorMsg = i18nState.getString(\"smax_needs_int\")\n                            showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.MEDIUM, errorMsg, errorMsg)\n                        })?: return@experimentViewClick\n                        val vmax = getValidateField(block = { vmaxText.toInt() } , failed = { \n                            val errorMsg = i18nState.getString(\"vmax_needs_int\")\n                            showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.MEDIUM, errorMsg, errorMsg)\n                        })?: return@experimentViewClick\n\n                        viewModel.inRange(state, hmin, smin, vmin, hmax, smax, vmax)\n                    }\n                }\n            ) {\n                Text(text = i18nState.getString(\"color_image_segmentation\"), color = Color.Unspecified)\n            }\n        }\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/ai/experiment/CVState.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.ai.experiment\n\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport cn.netdiscovery.monica.domain.ContourDisplaySettings\nimport cn.netdiscovery.monica.domain.ContourFilterSettings\nimport cn.netdiscovery.monica.domain.MatchTemplateSettings\nimport cn.netdiscovery.monica.domain.MorphologicalOperationSettings\nimport java.awt.image.BufferedImage\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.ai.experiment.CVState\n * @author: Tony Shen\n * @date: 2024/10/27 18:40\n * @version: V1.0 <描述当前版本功能>\n */\nobject CVState {\n\n    var isThreshType by mutableStateOf(false)\n\n    var isThreshSegment by mutableStateOf(false)\n\n    var isAdaptiveThresh by mutableStateOf(false)\n\n    var isFirstDerivativeOperator by mutableStateOf(false)\n\n    var isSecondDerivativeOperator by mutableStateOf(false)\n\n    var isCannyOperator by mutableStateOf(false)\n\n    var isContourPerimeter by mutableStateOf(false)\n\n    var isContourArea by mutableStateOf(false)\n\n    var isContourRoundness by mutableStateOf(false)\n\n    var isContourAspectRatio by mutableStateOf(false)\n\n    var showOriginalImage by mutableStateOf(false)\n\n    var showBoundingRect by mutableStateOf(false)\n\n    var showMinAreaRect by mutableStateOf(false)\n\n    var showCenter by mutableStateOf(false)\n\n    var templateImage: BufferedImage? by mutableStateOf(null)\n\n    /**\n     * 清空状态\n     */\n    fun clearAllStatus() {\n        isThreshType = false\n        isThreshSegment = false\n        isAdaptiveThresh = false\n        isFirstDerivativeOperator = false\n        isSecondDerivativeOperator = false\n        isCannyOperator = false\n        isContourPerimeter = false\n        isContourArea = false\n        isContourRoundness = false\n        isContourAspectRatio = false\n        showOriginalImage = false\n        showBoundingRect = false\n        showMinAreaRect = false\n        showCenter = false\n        templateImage = null\n\n        contourFilterSettings          = ContourFilterSettings()\n        contourDisplaySettings         = ContourDisplaySettings()\n        morphologicalOperationSettings = MorphologicalOperationSettings()\n        matchTemplateSettings          = MatchTemplateSettings()\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/ai/experiment/ContourAnalysisView.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.ai.experiment\n\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.material.Button\nimport androidx.compose.material.Text\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.unit.dp\nimport cn.netdiscovery.monica.state.ApplicationState\nimport cn.netdiscovery.monica.domain.ContourDisplaySettings\nimport cn.netdiscovery.monica.ui.i18n.rememberI18nState\nimport cn.netdiscovery.monica.domain.ContourFilterSettings\nimport cn.netdiscovery.monica.ui.controlpanel.ai.experiment.viewmodel.ContourAnalysisViewModel\nimport cn.netdiscovery.monica.ui.widget.*\nimport cn.netdiscovery.monica.utils.getValidateField\nimport cn.netdiscovery.monica.exception.showError\nimport cn.netdiscovery.monica.exception.ErrorType\nimport cn.netdiscovery.monica.exception.ErrorSeverity\nimport org.koin.compose.koinInject\nimport org.slf4j.Logger\nimport org.slf4j.LoggerFactory\nimport java.awt.image.BufferedImage\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.ai.experiment.ContourAnalysisView\n * @author: Tony Shen\n * @date: 2024/10/25 23:52\n * @version: V1.0 <描述当前版本功能>\n */\nprivate val logger: Logger = LoggerFactory.getLogger(object : Any() {}.javaClass.enclosingClass)\n\nvar contourFilterSettings:ContourFilterSettings = ContourFilterSettings()\nvar contourDisplaySettings:ContourDisplaySettings = ContourDisplaySettings()\n\n@Composable\nfun contourAnalysis(state: ApplicationState, title: String) {\n    val i18nState = rememberI18nState()\n    val viewModel: ContourAnalysisViewModel = koinInject()\n\n    var minPerimeterText by remember { mutableStateOf(\"\") }\n    var maxPerimeterText by remember { mutableStateOf(\"\") }\n\n    var minAreaText by remember { mutableStateOf(\"\") }\n    var maxAreaText by remember { mutableStateOf(\"\") }\n\n    var minRoundnessText by remember { mutableStateOf(\"\") }\n    var maxRoundnessText by remember { mutableStateOf(\"\") }\n\n    var minAspectRatioText by remember { mutableStateOf(\"\") }\n    var maxAspectRatioText by remember { mutableStateOf(\"\") }\n\n    fun clearContourPerimeterParams() {\n        minPerimeterText = \"\"\n        maxPerimeterText = \"\"\n\n        contourFilterSettings.minPerimeter = 0.0\n        contourFilterSettings.maxPerimeter = 0.0\n    }\n\n    fun clearContourAreaParams() {\n        minAreaText = \"\"\n        maxAreaText = \"\"\n\n        contourFilterSettings.minArea = 0.0\n        contourFilterSettings.maxArea = 0.0\n    }\n\n    fun clearContourRoundnessParams() {\n        minRoundnessText = \"\"\n        maxRoundnessText = \"\"\n\n        contourFilterSettings.minRoundness = 0.0\n        contourFilterSettings.maxRoundness = 0.0\n    }\n\n    fun clearContourAspectRatioParams() {\n        minAspectRatioText = \"\"\n        maxAspectRatioText = \"\"\n\n        contourFilterSettings.minAspectRatio = 0.0\n        contourFilterSettings.maxAspectRatio = 0.0\n    }\n\n    Column (modifier = Modifier.fillMaxSize().padding(start = 20.dp, end =  20.dp, top = 10.dp)) {\n        title(modifier = Modifier.align(Alignment.CenterHorizontally) , text = title, color = Color.Black)\n\n        Column{\n            subTitleWithDivider(text = i18nState.getString(\"filter_settings\"), color = Color.Black)\n\n            Row(verticalAlignment = Alignment.CenterVertically) {\n                checkBoxWithTitle(i18nState.getString(\"perimeter\"), Modifier.padding(end = 50.dp), checked = CVState.isContourPerimeter, onCheckedChange = {\n                    CVState.isContourPerimeter = it\n\n                    if (!CVState.isContourPerimeter) {\n                        clearContourPerimeterParams()\n                    }\n                })\n\n                basicTextFieldWithTitle(titleText = i18nState.getString(\"min_value\"), minPerimeterText) { str ->\n                    if (CVState.isContourPerimeter) {\n                        minPerimeterText = str\n\n                        contourFilterSettings.minPerimeter = getValidateField(block = { minPerimeterText.toDouble() } , failed = { \n                            val errorMsg = i18nState.getString(\"perimeter_min_needs_double\")\n                            showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.MEDIUM, errorMsg, errorMsg)\n                        }) ?: return@basicTextFieldWithTitle\n                    }\n                }\n\n                basicTextFieldWithTitle(titleText = i18nState.getString(\"max_value\"), maxPerimeterText) { str ->\n                    if (CVState.isContourPerimeter) {\n                        maxPerimeterText = str\n\n                        contourFilterSettings.maxPerimeter = getValidateField(block = { maxPerimeterText.toDouble() } , failed = { \n                            val errorMsg = i18nState.getString(\"perimeter_max_needs_double\")\n                            showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.MEDIUM, errorMsg, errorMsg)\n                        }) ?: return@basicTextFieldWithTitle\n                    }\n                }\n            }\n\n            Row(verticalAlignment = Alignment.CenterVertically) {\n                checkBoxWithTitle(i18nState.getString(\"area\"), Modifier.padding(end = 50.dp), checked = CVState.isContourArea, onCheckedChange = {\n                    CVState.isContourArea = it\n\n                    if (!CVState.isContourArea) {\n                        clearContourAreaParams()\n                    }\n                })\n\n                basicTextFieldWithTitle(titleText = i18nState.getString(\"min_value\"), minAreaText) { str ->\n                    if (CVState.isContourArea) {\n                        minAreaText = str\n\n                        contourFilterSettings.minArea = getValidateField(block = { minAreaText.toDouble() } , failed = { \n                            val errorMsg = i18nState.getString(\"area_min_needs_double\")\n                            showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.MEDIUM, errorMsg, errorMsg)\n                        }) ?: return@basicTextFieldWithTitle\n                    }\n                }\n\n                basicTextFieldWithTitle(titleText = i18nState.getString(\"max_value\"), maxAreaText) { str ->\n                    if (CVState.isContourArea) {\n                        maxAreaText = str\n\n                        contourFilterSettings.maxArea = getValidateField(block = { maxAreaText.toDouble() } , failed = { \n                            val errorMsg = i18nState.getString(\"area_max_needs_double\")\n                            showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.MEDIUM, errorMsg, errorMsg)\n                        }) ?: return@basicTextFieldWithTitle\n                    }\n                }\n            }\n\n            Row(verticalAlignment = Alignment.CenterVertically) {\n                checkBoxWithTitle(i18nState.getString(\"roundness\"), Modifier.padding(end = 50.dp), checked = CVState.isContourRoundness, onCheckedChange = {\n                    CVState.isContourRoundness = it\n\n                    if (!CVState.isContourRoundness) {\n                        clearContourRoundnessParams()\n                    }\n                })\n\n                basicTextFieldWithTitle(titleText = i18nState.getString(\"min_value\"), minRoundnessText) { str ->\n                    if (CVState.isContourRoundness) {\n                        minRoundnessText = str\n\n                        contourFilterSettings.minRoundness = getValidateField(block = { minRoundnessText.toDouble() } , failed = { \n                            val errorMsg = i18nState.getString(\"roundness_min_needs_double\")\n                            showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.MEDIUM, errorMsg, errorMsg)\n                        }) ?: return@basicTextFieldWithTitle\n                    }\n                }\n\n                basicTextFieldWithTitle(titleText = i18nState.getString(\"max_value\"), maxRoundnessText) { str ->\n                    if (CVState.isContourRoundness) {\n                        maxRoundnessText = str\n\n                        contourFilterSettings.maxRoundness = getValidateField(block = { maxRoundnessText.toDouble() } , failed = { \n                            val errorMsg = i18nState.getString(\"roundness_max_needs_double\")\n                            showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.MEDIUM, errorMsg, errorMsg)\n                        }) ?: return@basicTextFieldWithTitle\n                    }\n                }\n            }\n\n            Row(verticalAlignment = Alignment.CenterVertically) {\n                checkBoxWithTitle(i18nState.getString(\"aspect_ratio\"), Modifier.padding(end = 35.dp), checked = CVState.isContourAspectRatio, onCheckedChange = {\n                    CVState.isContourAspectRatio = it\n\n                    if (!CVState.isContourAspectRatio) {\n                        clearContourAspectRatioParams()\n                    }\n                })\n\n                basicTextFieldWithTitle(titleText = i18nState.getString(\"min_value\"), minAspectRatioText) { str ->\n                    if (CVState.isContourAspectRatio) {\n                        minAspectRatioText = str\n\n                        contourFilterSettings.minAspectRatio = getValidateField(block = { minAspectRatioText.toDouble() } , failed = { \n                            val errorMsg = i18nState.getString(\"aspect_ratio_min_needs_double\")\n                            showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.MEDIUM, errorMsg, errorMsg)\n                        }) ?: return@basicTextFieldWithTitle\n                    }\n                }\n\n                basicTextFieldWithTitle(titleText = i18nState.getString(\"max_value\"), maxAspectRatioText) { str ->\n                    if (CVState.isContourAspectRatio) {\n                        maxAspectRatioText = str\n\n                        contourFilterSettings.maxAspectRatio = getValidateField(block = { maxAspectRatioText.toDouble() } , failed = { \n                            val errorMsg = i18nState.getString(\"aspect_ratio_max_needs_double\")\n                            showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.MEDIUM, errorMsg, errorMsg)\n                        }) ?: return@basicTextFieldWithTitle\n                    }\n                }\n            }\n        }\n\n        Column(modifier = Modifier.padding(top = 20.dp)) {\n            subTitleWithDivider(text = i18nState.getString(\"contour_display_settings\"), color = Color.Black)\n\n            Row(verticalAlignment = Alignment.CenterVertically) {\n                checkBoxWithTitle(i18nState.getString(\"show_original_image\"), Modifier.padding(end = 50.dp), checked = CVState.showOriginalImage, onCheckedChange = {\n                    contourDisplaySettings.showOriginalImage = it\n                    CVState.showOriginalImage = it\n                })\n\n                checkBoxWithTitle(i18nState.getString(\"show_bounding_rect\"), Modifier.padding(end = 50.dp), checked = CVState.showBoundingRect, onCheckedChange = {\n                    contourDisplaySettings.showBoundingRect = it\n                    CVState.showBoundingRect = it\n                })\n\n                checkBoxWithTitle(i18nState.getString(\"show_min_area_rect\"),Modifier.padding(end = 50.dp), checked = CVState.showMinAreaRect, onCheckedChange = {\n                    contourDisplaySettings.showMinAreaRect = it\n                    CVState.showMinAreaRect = it\n                })\n\n                checkBoxWithTitle(i18nState.getString(\"show_center\"),Modifier.padding(end = 50.dp), checked = CVState.showCenter, onCheckedChange = {\n                    contourDisplaySettings.showCenter = it\n                    CVState.showCenter = it\n                })\n            }\n        }\n\n        Button(\n            modifier = Modifier.padding(top = 10.dp).align(Alignment.End),\n            onClick = experimentViewClick(state) {\n\n                if(state.currentImage?.type == BufferedImage.TYPE_BYTE_BINARY) {\n                    if (CVState.isContourPerimeter) {\n                        if (contourFilterSettings.minPerimeter == 0.0 && contourFilterSettings.maxPerimeter == 0.0) {\n                            val errorMsg = i18nState.getString(\"perimeter_at_least_one_value\")\n                            showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.MEDIUM, errorMsg, errorMsg)\n                            return@experimentViewClick\n                        }\n                    }\n\n                    if (CVState.isContourArea) {\n                        if (contourFilterSettings.minArea == 0.0 && contourFilterSettings.maxArea == 0.0) {\n                            val errorMsg = i18nState.getString(\"area_at_least_one_value\")\n                            showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.MEDIUM, errorMsg, errorMsg)\n                            return@experimentViewClick\n                        }\n                    }\n\n                    if (CVState.isContourRoundness) {\n                        if (contourFilterSettings.minRoundness == 0.0 && contourFilterSettings.maxRoundness == 0.0) {\n                            val errorMsg = i18nState.getString(\"roundness_at_least_one_value\")\n                            showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.MEDIUM, errorMsg, errorMsg)\n                            return@experimentViewClick\n                        }\n                    }\n\n                    if (CVState.isContourAspectRatio) {\n                        if (contourFilterSettings.minAspectRatio == 0.0 && contourFilterSettings.maxAspectRatio == 0.0) {\n                            val errorMsg = i18nState.getString(\"aspect_ratio_at_least_one_value\")\n                            showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.MEDIUM, errorMsg, errorMsg)\n                            return@experimentViewClick\n                        }\n                    }\n\n                    viewModel.contourAnalysis(state, contourFilterSettings, contourDisplaySettings)\n                } else {\n                    val errorMsg = i18nState.getString(\"please_binarize_image_first_for_contour\")\n                    showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.MEDIUM, errorMsg, errorMsg)\n                }\n            }\n        ) {\n            Text(text = i18nState.getString(\"contour_analysis\"), color = Color.Unspecified)\n        }\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/ai/experiment/EdgeDetectionView.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.ai.experiment\n\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.material.Button\nimport androidx.compose.material.Checkbox\nimport androidx.compose.material.RadioButton\nimport androidx.compose.material.Text\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.unit.dp\nimport cn.netdiscovery.monica.state.ApplicationState\nimport cn.netdiscovery.monica.ui.controlpanel.ai.experiment.viewmodel.EdgeDetectionViewModel\nimport cn.netdiscovery.monica.ui.i18n.rememberI18nState\nimport cn.netdiscovery.monica.i18n.LocalizationManager\nimport cn.netdiscovery.monica.ui.widget.*\nimport cn.netdiscovery.monica.utils.getValidateField\nimport cn.netdiscovery.monica.exception.showError\nimport cn.netdiscovery.monica.exception.ErrorType\nimport cn.netdiscovery.monica.exception.ErrorSeverity\nimport org.koin.compose.koinInject\nimport org.slf4j.Logger\nimport org.slf4j.LoggerFactory\nimport java.awt.image.BufferedImage\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.ai.experiment.EdgeDetectionView\n * @author: Tony Shen\n * @date:  2024/10/13 22:17\n * @version: V1.0 <描述当前版本功能>\n */\nprivate val logger: Logger = LoggerFactory.getLogger(object : Any() {}.javaClass.enclosingClass)\n\nval firstDerivativeOperatorTags = arrayListOf(LocalizationManager.getString(\"roberts_operator\"), LocalizationManager.getString(\"prewitt_operator\"), LocalizationManager.getString(\"sobel_operator\"))\nval secondDerivativeOperatorTags = arrayListOf(LocalizationManager.getString(\"laplace_operator\"), LocalizationManager.getString(\"log_operator\"), LocalizationManager.getString(\"dog_operator\"))\n\n@Composable\nfun edgeDetection(state: ApplicationState, title: String) {\n    val i18nState = rememberI18nState()\n    val viewModel: EdgeDetectionViewModel = koinInject()\n\n    var firstDerivativeOperatorSelectedOption  by remember { mutableStateOf(\"Null\") }\n    var secondDerivativeOperatorSelectedOption by remember { mutableStateOf(\"Null\") }\n\n    var threshold1Text by remember { mutableStateOf(\"\") }\n    var threshold2Text by remember { mutableStateOf(\"\") }\n    var apertureSizeText by remember { mutableStateOf(\"3\") }\n\n    var sigma1Text by remember { mutableStateOf(\"\") }\n    var sigma2Text by remember { mutableStateOf(\"\") }\n    var sizeText by remember { mutableStateOf(\"\") }\n\n    fun clearCannyParams() {\n        threshold1Text = \"\"\n        threshold2Text = \"\"\n        apertureSizeText = \"3\"\n    }\n\n    fun clearDoGParams() {\n        sigma1Text = \"\"\n        sigma2Text = \"\"\n        sizeText = \"\"\n    }\n\n    Column (modifier = Modifier.fillMaxSize().padding(start = 20.dp, end =  20.dp, top = 10.dp)) {\n        title(modifier = Modifier.align(Alignment.CenterHorizontally) , text = title, color = Color.Black)\n\n        Column{\n            subTitleWithDivider(text = i18nState.getString(\"edge_detection_operator\"), color = Color.Black)\n\n            Row(verticalAlignment = Alignment.CenterVertically) {\n                Checkbox(CVState.isFirstDerivativeOperator, onCheckedChange = {\n                    CVState.isFirstDerivativeOperator = it\n\n                    if (!CVState.isFirstDerivativeOperator) {\n                        firstDerivativeOperatorSelectedOption = \"Null\"\n                    } else {\n                        CVState.isSecondDerivativeOperator = false\n                        CVState.isCannyOperator = false\n                        clearCannyParams()\n                        clearDoGParams()\n                    }\n                })\n                Text(i18nState.getString(\"first_derivative_operator\"), modifier = Modifier.align(Alignment.CenterVertically))\n            }\n\n            Row {\n                firstDerivativeOperatorTags.forEach {\n                    RadioButton(\n                        selected = (CVState.isFirstDerivativeOperator && it == firstDerivativeOperatorSelectedOption),\n                        onClick = {\n                            firstDerivativeOperatorSelectedOption = it\n                        }\n                    )\n\n                    Text(text = it, modifier = Modifier.align(Alignment.CenterVertically))\n                }\n\n                Column(modifier = Modifier.fillMaxWidth(),\n                    horizontalAlignment = Alignment.End,\n                    verticalArrangement = Arrangement.Center) {\n                    Button(\n                        onClick = experimentViewClick(state) {\n                            if (firstDerivativeOperatorSelectedOption == \"Null\") {\n                                val errorMsg = i18nState.getString(\"please_select_first_derivative_operator\")\n                                showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.MEDIUM, errorMsg, errorMsg)\n                                return@experimentViewClick\n                            }\n\n                            when(firstDerivativeOperatorSelectedOption) {\n                                firstDerivativeOperatorTags[0] -> viewModel.roberts(state)\n                                firstDerivativeOperatorTags[1] -> viewModel.prewitt(state)\n                                firstDerivativeOperatorTags[2] -> viewModel.sobel(state)\n                                else         -> {}\n                            }\n                        }\n                    ) {\n                        Text(text = i18nState.getString(\"first_derivative_edge_detection\"), color = Color.Unspecified)\n                    }\n                }\n            }\n\n            Row(modifier = Modifier.padding(top = 10.dp),verticalAlignment = Alignment.CenterVertically) {\n                Checkbox(CVState.isSecondDerivativeOperator, onCheckedChange = {\n                    CVState.isSecondDerivativeOperator = it\n\n                    if (!CVState.isSecondDerivativeOperator) {\n                        secondDerivativeOperatorSelectedOption = \"Null\"\n                    } else {\n                        CVState.isFirstDerivativeOperator = false\n                        CVState.isCannyOperator = false\n                        clearCannyParams()\n                    }\n                })\n                Text(i18nState.getString(\"second_derivative_operator\"), modifier = Modifier.align(Alignment.CenterVertically))\n            }\n\n            Row {\n                secondDerivativeOperatorTags.forEach {\n                    RadioButton(\n                        selected = (CVState.isSecondDerivativeOperator && it == secondDerivativeOperatorSelectedOption),\n                        onClick = {\n                            secondDerivativeOperatorSelectedOption = it\n                        }\n                    )\n\n                    Text(text = it, modifier = Modifier.align(Alignment.CenterVertically))\n                }\n\n                Column(modifier = Modifier.fillMaxWidth(),\n                    horizontalAlignment = Alignment.End,\n                    verticalArrangement = Arrangement.Center) {\n                    Button(\n                        onClick = experimentViewClick(state) {\n                            if (secondDerivativeOperatorSelectedOption == \"Null\") {\n                                val errorMsg = i18nState.getString(\"please_select_second_derivative_operator\")\n                                showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.MEDIUM, errorMsg, errorMsg)\n                                return@experimentViewClick\n                            }\n\n                            when(secondDerivativeOperatorSelectedOption) {\n                                secondDerivativeOperatorTags[0] -> viewModel.laplace(state)\n                                secondDerivativeOperatorTags[1] -> viewModel.log(state)\n                                secondDerivativeOperatorTags[2] -> {\n                                    val sigma1 = getValidateField(block = { sigma1Text.toDouble() } , failed = { \n                                        val errorMsg = i18nState.getString(\"sigma1_needs_double\")\n                                        showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.MEDIUM, errorMsg, errorMsg)\n                                    }) ?: return@experimentViewClick\n                                    val sigma2 = getValidateField(block = { sigma2Text.toDouble() } , failed = { \n                                        val errorMsg = i18nState.getString(\"sigma2_needs_double\")\n                                        showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.MEDIUM, errorMsg, errorMsg)\n                                    }) ?: return@experimentViewClick\n                                    val size = getValidateField(block = { sizeText.toInt() } , failed = { \n                                        val errorMsg = i18nState.getString(\"size_needs_int\")\n                                        showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.MEDIUM, errorMsg, errorMsg)\n                                    }) ?: return@experimentViewClick\n                                    viewModel.dog(state, sigma1, sigma2, size)\n                                }\n                                else         -> {}\n                            }\n                        }\n                    ) {\n                        Text(text = i18nState.getString(\"second_derivative_edge_detection\"), color = Color.Unspecified)\n                    }\n                }\n            }\n\n            if (CVState.isSecondDerivativeOperator && secondDerivativeOperatorSelectedOption == secondDerivativeOperatorTags[2]) {\n                Row {\n                    basicTextFieldWithTitle(titleText = \"sigma1\", sigma1Text) { str ->\n                        if (CVState.isSecondDerivativeOperator) {\n                            sigma1Text = str\n                        }\n                    }\n\n                    basicTextFieldWithTitle(titleText = \"sigma2\", sigma2Text) { str ->\n                        if (CVState.isSecondDerivativeOperator) {\n                            sigma2Text = str\n                        }\n                    }\n\n                    basicTextFieldWithTitle(titleText = \"size\", sizeText) { str ->\n                        if (CVState.isSecondDerivativeOperator) {\n                            sizeText = str\n                        }\n                    }\n                }\n            }\n\n            Row(modifier = Modifier.padding(top = 10.dp), verticalAlignment = Alignment.CenterVertically) {\n                Checkbox(CVState.isCannyOperator, onCheckedChange = {\n                    CVState.isCannyOperator = it\n\n                    if (!CVState.isCannyOperator) {\n                        clearCannyParams()\n                    } else {\n                        CVState.isFirstDerivativeOperator = false\n                        CVState.isSecondDerivativeOperator = false\n                        clearDoGParams()\n                    }\n                })\n                Text(i18nState.getString(\"canny_operator\"), modifier = Modifier.align(Alignment.CenterVertically))\n            }\n\n            Row(modifier = Modifier.padding(top = 10.dp)) {\n                basicTextFieldWithTitle(titleText = \"threshold1\", threshold1Text) { str ->\n                    threshold1Text = str\n                }\n\n                basicTextFieldWithTitle(titleText = \"threshold2\", threshold2Text) { str ->\n                    threshold2Text = str\n                }\n\n                basicTextFieldWithTitle(titleText = \"apertureSize\", apertureSizeText) { str ->\n                    apertureSizeText = str\n                }\n            }\n\n            Button(\n                modifier = Modifier.padding(top = 10.dp).align(Alignment.End),\n                onClick = experimentViewClick(state) {\n                    if(state.currentImage?.type != BufferedImage.TYPE_BYTE_BINARY) {\n                                        val threshold1 = getValidateField(block = { threshold1Text.toDouble() }, failed = { \n                                            val errorMsg = i18nState.getString(\"threshold1_needs_double\")\n                                            showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.MEDIUM, errorMsg, errorMsg)\n                                        }) ?: return@experimentViewClick\n                val threshold2 = getValidateField(block = { threshold2Text.toDouble() }, failed = { \n                    val errorMsg = i18nState.getString(\"threshold2_needs_double\")\n                    showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.MEDIUM, errorMsg, errorMsg)\n                }) ?: return@experimentViewClick\n                val apertureSize = getValidateField(block = { apertureSizeText.toInt() }, failed = { \n                    val errorMsg = i18nState.getString(\"aperture_size_needs_int\")\n                    showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.MEDIUM, errorMsg, errorMsg)\n                }) ?: return@experimentViewClick\n\n                        viewModel.canny(state, threshold1, threshold2, apertureSize)\n                    }\n                }\n            ) {\n                Text(text = i18nState.getString(\"canny_edge_detection_full\"), color = Color.Unspecified)\n            }\n        }\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/ai/experiment/ExperimentHome.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.ai.experiment\n\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.unit.dp\nimport cn.netdiscovery.monica.ui.widget.subTitle\nimport cn.netdiscovery.monica.ui.i18n.rememberI18nState\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.ai.experiment.ExperimentHome\n * @author: Tony Shen\n * @date: 2024/11/2 00:27\n * @version: V1.0 <描述当前版本功能>\n */\n\n@Composable\nfun experimentHome() {\n    val i18nState = rememberI18nState()\n\n    Column (modifier = Modifier.fillMaxSize().padding(start = 20.dp, end =  20.dp, top = 10.dp),\n            verticalArrangement = Arrangement.Center,\n            horizontalAlignment = Alignment.CenterHorizontally\n    ) {\n        Image(painter = painterResource(\"images/ai/OpenCV_Logo.png\"),\n            contentDescription = null,\n            modifier = Modifier)\n\n        subTitle(modifier = Modifier.padding(top = 20.dp),\n            text = i18nState.getString(\"experiment_home_description\"))\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/ai/experiment/ExperimentView.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.ai.experiment\n\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.*\nimport androidx.compose.material.MaterialTheme\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.graphics.toPainter\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.unit.dp\nimport cn.netdiscovery.monica.state.ApplicationState\nimport cn.netdiscovery.monica.ui.widget.*\nimport cn.netdiscovery.monica.utils.Action\nimport cn.netdiscovery.monica.utils.chooseImage\nimport cn.netdiscovery.monica.utils.getBufferedImage\nimport loadingDisplay\nimport cn.netdiscovery.monica.ui.i18n.rememberI18nState\nimport cn.netdiscovery.monica.i18n.LocalizationManager\nimport cn.netdiscovery.monica.exception.ErrorHandler\nimport cn.netdiscovery.monica.exception.ErrorState\nimport org.slf4j.Logger\nimport org.slf4j.LoggerFactory\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.ai.experiment.ExperimentView\n * @author: Tony Shen\n * @date: 2024/9/23 19:37\n * @version: V1.0 <描述当前版本功能>\n */\nprivate val logger: Logger = LoggerFactory.getLogger(object : Any() {}.javaClass.enclosingClass)\n\n/**\n * Screens\n */\nenum class Screen(\n    private val labelKey: String,\n    val resourcePath: String\n) {\n    Home(\n        labelKey = \"experiment_home\",\n        resourcePath = \"images/ai/home.png\"\n    ),\n    BinaryImage(\n        labelKey = \"experiment_binary_image\",\n        resourcePath = \"images/ai/binary_image.png\"\n    ),\n    EdgeDetection(\n        labelKey = \"experiment_edge_detection\",\n        resourcePath = \"images/ai/edge_detection.png\"\n    ),\n    ContourAnalysis(\n        labelKey = \"experiment_contour_analysis\",\n        resourcePath = \"images/ai/contour_analysis.png\"\n    ),\n    ImageEnhance(\n        labelKey = \"experiment_image_enhance\",\n        resourcePath = \"images/ai/image_enhance.png\"\n    ),\n    ImageDenoising(\n        labelKey = \"experiment_image_denoising\",\n        resourcePath = \"images/ai/image_convolution.png\"\n    ),\n    MorphologicalOperations(\n        labelKey = \"experiment_morphological_operations\",\n        resourcePath = \"images/ai/morphological_operations.png\"\n    ),\n    MatchTemplate(\n        labelKey = \"experiment_match_template\",\n        resourcePath = \"images/ai/match_template.png\"\n    ),\n    History(\n        labelKey = \"experiment_history\",\n        resourcePath = \"images/ai/history.png\"\n    );\n    \n    fun getLabel(): String {\n        return LocalizationManager.getString(labelKey)\n    }\n}\n\n@Composable\nfun customNavigationHost(\n    state: ApplicationState,\n    navController: NavController\n) {\n    NavigationHost(navController) {\n        composable(Screen.Home.name) {\n            experimentHome()\n        }\n\n        composable(Screen.BinaryImage.name) {\n            binaryImage(state, Screen.BinaryImage.getLabel())\n        }\n\n        composable(Screen.EdgeDetection.name) {\n            edgeDetection(state, Screen.EdgeDetection.getLabel())\n        }\n\n        composable(Screen.ContourAnalysis.name) {\n            contourAnalysis(state, Screen.ContourAnalysis.getLabel())\n        }\n\n        composable(Screen.ImageEnhance.name) {\n            imageEnhance(state, Screen.ImageEnhance.getLabel())\n        }\n\n        composable(Screen.ImageDenoising.name) {\n            imageDenoising(state, Screen.ImageDenoising.getLabel())\n        }\n\n        composable(Screen.MorphologicalOperations.name) {\n            morphologicalOperations(state, Screen.MorphologicalOperations.getLabel())\n        }\n\n        composable(Screen.MatchTemplate.name) {\n            matchTemplate(state, Screen.MatchTemplate.getLabel())\n        }\n\n        composable(Screen.History.name) {\n            history(state, Screen.History.getLabel())\n        }\n    }.build()\n}\n\n@OptIn(ExperimentalMaterialApi::class)\n@Composable\nfun experiment(state: ApplicationState) {\n    val i18nState = rememberI18nState()\n    \n    // 页面级别的错误处理状态\n    val errorState = remember { ErrorState() }\n\n    val screens = Screen.entries\n    val navController by rememberNavController(Screen.Home.name)\n    val currentScreen by remember { navController.currentScreen }\n\n    PageLifecycle(\n        onInit = {\n            logger.info(\"OpenCVDebugView 启动时初始化\")\n        },\n        onDisposeEffect = {\n            logger.info(\"OpenCVDebugView 关闭时释放资源\")\n            CVState.clearAllStatus()\n        }\n    )\n\n    Box(\n        modifier = Modifier\n            .fillMaxSize()\n            .background(\n                brush = Brush.verticalGradient(\n                    colors = listOf(\n                        MaterialTheme.colors.background,\n                        MaterialTheme.colors.surface\n                    )\n                )\n            ),\n        contentAlignment = Alignment.Center\n    ) {\n        Row (\n            modifier = Modifier.fillMaxSize(),\n            verticalAlignment = Alignment.CenterVertically,\n            horizontalArrangement = Arrangement.Center\n        ) {\n            NavigationRail(\n                modifier = Modifier.fillMaxHeight().width(100.dp).weight(0.5f)\n            ) {\n                screens.forEach {\n                    NavigationRailItem(\n                        selected = currentScreen == it.name,\n                        icon = {\n                            Icon(\n                                painter = painterResource(it.resourcePath),\n                                modifier = Modifier.width(25.dp).height(25.dp),\n                                contentDescription = it.getLabel()\n                            )\n                        },\n                        label = {\n                            Text(it.getLabel())\n                        },\n                        modifier = Modifier.width(100.dp).height(80.dp),\n                        alwaysShowLabel = true,\n                        onClick = {\n                            navController.navigate(it.name)\n                        }\n                    )\n                }\n            }\n\n            Box(\n                Modifier.fillMaxSize().weight(9.5f),\n                contentAlignment = Alignment.Center\n            ) {\n                Row (modifier = Modifier.fillMaxSize().padding(end = 90.dp),\n                    verticalAlignment = Alignment.CenterVertically,\n                    horizontalArrangement = Arrangement.Center) {\n                    Column (modifier = Modifier.fillMaxSize().weight(1.0f),\n                        verticalArrangement = Arrangement.Center,\n                        horizontalAlignment = Alignment.CenterHorizontally) {\n                        customNavigationHost(state, navController)\n                    }\n\n                    Card(\n                        modifier = Modifier.padding(10.dp).weight(1.0f),\n                        shape = RoundedCornerShape(8.dp),\n                        elevation = 4.dp,\n                        onClick = {\n                            chooseImage(state) { file ->\n                                state.rawImage = getBufferedImage(file, state)\n                                state.currentImage = state.rawImage\n                                state.rawImageFile = file\n                            }\n                        },\n                        enabled = state.currentImage == null\n                    ) {\n                        if (state.currentImage == null) {\n                            Text(\n                                modifier = Modifier.fillMaxSize().wrapContentSize(Alignment.Center),\n                                text = i18nState.getString(\"click_to_select_image\"),\n                                textAlign = TextAlign.Center\n                            )\n                        } else {\n                            Image(\n                                painter = state.currentImage!!.toPainter(),\n                                contentDescription = null,\n                                contentScale = ContentScale.Fit,\n                                modifier = Modifier\n                            )\n                        }\n                    }\n                }\n\n                rightSideMenuBar(modifier = Modifier.align(Alignment.CenterEnd)) {\n                    toolTipButton(text = i18nState.getString(\"delete\"),\n                        painter = painterResource(\"images/preview/delete.png\"),\n                        iconModifier = Modifier.size(36.dp),\n                        onClick = {\n                            state.clearImage()\n                        })\n\n                    toolTipButton(text = i18nState.getString(\"undo\"),\n                        painter = painterResource(\"images/doodle/previous_step.png\"),\n                        iconModifier = Modifier.size(36.dp),\n                        onClick = {\n                            state.getLastImage()?.let {\n                                state.currentImage = it\n                            }\n                        })\n\n                    toolTipButton(text = i18nState.getString(\"save\"),\n                        painter = painterResource(\"images/doodle/save.png\"),\n                        iconModifier = Modifier.size(36.dp),\n                        onClick = {\n                            state.togglePreviewWindow(false)\n                        })\n                }\n            }\n        }\n\n        if (loadingDisplay) {\n            showLoading()\n        }\n        \n        // 页面级别的错误处理 - 在最后渲染，确保在最顶层\n        ErrorHandler(errorState)\n    }\n}\n\n@Composable\nfun experimentViewClick(\n    state: ApplicationState,\n    onClick: Action\n): Action {\n    val i18nState = rememberI18nState()\n    \n    return rememberThrottledClick(filter = {\n        if (state.currentImage == null) {\n            val errorMsg = i18nState.getString(\"please_select_image_first\")\n            cn.netdiscovery.monica.exception.showError(\n                type = cn.netdiscovery.monica.exception.ErrorType.VALIDATION_ERROR,\n                severity = cn.netdiscovery.monica.exception.ErrorSeverity.MEDIUM,\n                message = errorMsg,\n                userMessage = errorMsg\n            )\n            false\n        } else {\n            true\n        }\n    }, onClick = onClick)\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/ai/experiment/HistoryView.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.ai.experiment\n\nimport androidx.compose.foundation.lazy.*\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.material.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.mutableStateListOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport cn.netdiscovery.monica.history.HistoryEntry\nimport cn.netdiscovery.monica.state.ApplicationState\nimport cn.netdiscovery.monica.ui.controlpanel.ai.experiment.viewmodel.HistoryViewModel\nimport cn.netdiscovery.monica.ui.i18n.rememberI18nState\nimport cn.netdiscovery.monica.ui.widget.divider\nimport cn.netdiscovery.monica.ui.widget.title\nimport cn.netdiscovery.monica.utils.formatTimestamp\nimport org.koin.compose.koinInject\nimport org.slf4j.Logger\nimport org.slf4j.LoggerFactory\nimport java.util.Date\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.ai.experiment.HistoryView\n * @author: Tony Shen\n * @date: 2025/7/30 09:32\n * @version: V1.0 <描述当前版本功能>\n */\nprivate val logger: Logger = LoggerFactory.getLogger(object : Any() {}.javaClass.enclosingClass)\n\n@Composable\nfun history(state: ApplicationState, title: String) {\n    val i18nState = rememberI18nState()\n    val viewModel: HistoryViewModel = koinInject()\n\n    val historyEntries = remember { mutableStateListOf<HistoryEntry>() }\n\n    LaunchedEffect(Unit) {\n        historyEntries.clear()\n        historyEntries.addAll(viewModel.getOperationLog())\n    }\n\n    Column (modifier = Modifier.fillMaxSize().padding(start = 20.dp, end =  20.dp)) {\n        title(modifier = Modifier.align(Alignment.CenterHorizontally), text = title, color = Color.Black)\n\n        CVHistoryList(historyEntries, i18nState)\n    }\n}\n\n@Composable\nfun CVHistoryList(history: List<HistoryEntry>, i18nState: cn.netdiscovery.monica.ui.i18n.I18nState) {\n    Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {\n\n        LazyColumn(modifier = Modifier.fillMaxSize()) {\n            items(history) { entry ->\n                HistoryItem(entry, i18nState)\n                divider()\n            }\n        }\n    }\n}\n\n@Composable\nfun HistoryItem(entry: HistoryEntry, i18nState: cn.netdiscovery.monica.ui.i18n.I18nState) {\n    Column(modifier = Modifier.padding(start = 8.dp, end = 8.dp)) {\n        Text(\n            text = \"${i18nState.getString(\"operation\")}: ${entry.operation}\",\n        )\n        Text(\n            text = \"${i18nState.getString(\"time\")}: ${formatTimestamp.format(Date(entry.timestamp))}\",\n        )\n        Text(\n            text = \"${i18nState.getString(\"parameters\")}: ${entry.parameters.entries.joinToString { \"${it.key}=${it.value}\" }}\",\n            maxLines = 6,\n            overflow = TextOverflow.Ellipsis\n        )\n        if (entry.description.isNotEmpty()) {\n            Text(\n                text = \"${i18nState.getString(\"description\")}: ${entry.description}\"\n            )\n        }\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/ai/experiment/ImageDenoisingView.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.ai.experiment\n\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material.Button\nimport androidx.compose.material.Text\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.unit.dp\nimport cn.netdiscovery.monica.state.ApplicationState\nimport cn.netdiscovery.monica.ui.controlpanel.ai.experiment.viewmodel.ImageDenoisingViewModel\nimport cn.netdiscovery.monica.ui.i18n.rememberI18nState\nimport cn.netdiscovery.monica.ui.widget.basicTextFieldWithTitle\nimport cn.netdiscovery.monica.ui.widget.subTitleWithDivider\nimport cn.netdiscovery.monica.ui.widget.title\nimport cn.netdiscovery.monica.utils.getValidateField\nimport cn.netdiscovery.monica.exception.showError\nimport cn.netdiscovery.monica.exception.ErrorType\nimport cn.netdiscovery.monica.exception.ErrorSeverity\nimport org.koin.compose.koinInject\nimport org.slf4j.Logger\nimport org.slf4j.LoggerFactory\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.ai.experiment.ImageDenoisingView\n * @author: Tony Shen\n * @date: 2024/12/4 14:17\n * @version: V1.0 <描述当前版本功能>\n */\nprivate val logger: Logger = LoggerFactory.getLogger(object : Any() {}.javaClass.enclosingClass)\n\n\n@Composable\nfun imageDenoising(state: ApplicationState, title: String) {\n    val i18nState = rememberI18nState()\n    val viewModel: ImageDenoisingViewModel = koinInject()\n\n    var gaussianBlurKSizeText by remember { mutableStateOf(\"\") }\n    var sigmaXText by remember { mutableStateOf(\"0.0\") }\n    var sigmaYText by remember { mutableStateOf(\"0.0\") }\n\n    var medianBlurKSizeText by remember { mutableStateOf(\"\") }\n\n    var dText by remember { mutableStateOf(\"\") }\n    var sigmaColorText by remember { mutableStateOf(\"\") }\n    var sigmaSpaceText by remember { mutableStateOf(\"\") }\n\n    var spText by remember { mutableStateOf(\"\") }\n    var srText by remember { mutableStateOf(\"\") }\n\n    Column (modifier = Modifier.fillMaxSize().padding(start = 20.dp, end =  20.dp, top = 10.dp)) {\n        title(modifier = Modifier.align(Alignment.CenterHorizontally), text = title, color = Color.Black)\n\n        Column {\n            subTitleWithDivider(text = i18nState.getString(\"gaussian_filter\"), color = Color.Black)\n\n            Row(modifier = Modifier.padding(top = 10.dp)) {\n                basicTextFieldWithTitle(titleText = \"ksize\", gaussianBlurKSizeText) { str ->\n                    gaussianBlurKSizeText = str\n                }\n\n                basicTextFieldWithTitle(titleText = \"sigmaX\", sigmaXText) { str ->\n                    sigmaXText = str\n                }\n\n                basicTextFieldWithTitle(titleText = \"sigmaY\", sigmaYText) { str ->\n                    sigmaYText = str\n                }\n            }\n\n            Button(\n                modifier = Modifier.align(Alignment.End),\n                onClick = experimentViewClick(state) {\n\n                                val ksize = getValidateField(block = { gaussianBlurKSizeText.toInt() } , failed = { \n                                    val errorMsg = i18nState.getString(\"ksize_needs_int\")\n                                    showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.MEDIUM, errorMsg, errorMsg)\n                                }) ?: return@experimentViewClick\n            val sigmaX = getValidateField(block = { sigmaXText.toDouble() } , failed = { \n                val errorMsg = i18nState.getString(\"sigma_x_needs_double\")\n                showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.MEDIUM, errorMsg, errorMsg)\n            }) ?: return@experimentViewClick\n            val sigmaY = getValidateField(block = { sigmaYText.toDouble() } , failed = { \n                val errorMsg = i18nState.getString(\"sigma_y_needs_double\")\n                showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.MEDIUM, errorMsg, errorMsg)\n            }) ?: return@experimentViewClick\n                    viewModel.gaussianBlur(state, ksize, sigmaX, sigmaY)\n                }\n            ) {\n                Text(text = i18nState.getString(\"gaussian_filter\"), color = Color.Unspecified)\n            }\n        }\n\n        Column(modifier = Modifier.padding(top = 20.dp)) {\n            subTitleWithDivider(text = i18nState.getString(\"median_filter\"), color = Color.Black)\n\n            Row(modifier = Modifier.padding(top = 10.dp)) {\n                basicTextFieldWithTitle(titleText = \"ksize\", medianBlurKSizeText) { str ->\n                    medianBlurKSizeText = str\n                }\n            }\n\n            Button(\n                modifier = Modifier.align(Alignment.End),\n                onClick = experimentViewClick(state) {\n\n                    val ksize = getValidateField(block = { medianBlurKSizeText.toInt() } , failed = { \n                        val errorMsg = i18nState.getString(\"ksize_needs_int\")\n                        showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.MEDIUM, errorMsg, errorMsg)\n                    }) ?: return@experimentViewClick\n                    viewModel.medianBlur(state, ksize)\n                }\n            ) {\n                Text(text = i18nState.getString(\"median_filter\"), color = Color.Unspecified)\n            }\n        }\n\n        Column(modifier = Modifier.padding(top = 20.dp)) {\n            subTitleWithDivider(text = i18nState.getString(\"gaussian_bilateral_filter\"), color = Color.Black)\n\n            Row(modifier = Modifier.padding(top = 10.dp)) {\n                basicTextFieldWithTitle(titleText = \"d\", dText) { str ->\n                    dText = str\n                }\n\n                basicTextFieldWithTitle(titleText = \"sigmaColor\", sigmaColorText) { str ->\n                    sigmaColorText = str\n                }\n\n                basicTextFieldWithTitle(titleText = \"sigmaSpace\", sigmaSpaceText) { str ->\n                    sigmaSpaceText = str\n                }\n            }\n\n            Button(\n                modifier = Modifier.align(Alignment.End),\n                onClick = experimentViewClick(state) {\n\n                                val d = getValidateField(block = { dText.toInt() } , failed = { \n                                    val errorMsg = i18nState.getString(\"d_needs_int\")\n                                    showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.MEDIUM, errorMsg, errorMsg)\n                                }) ?: return@experimentViewClick\n            val sigmaColor = getValidateField(block = { sigmaColorText.toDouble() } , failed = { \n                val errorMsg = i18nState.getString(\"sigma_color_needs_double\")\n                showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.MEDIUM, errorMsg, errorMsg)\n            }) ?: return@experimentViewClick\n            val sigmaSpace = getValidateField(block = { sigmaSpaceText.toDouble() } , failed = { \n                val errorMsg = i18nState.getString(\"sigma_space_needs_double\")\n                showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.MEDIUM, errorMsg, errorMsg)\n            }) ?: return@experimentViewClick\n                    viewModel.bilateralFilter(state, d, sigmaColor, sigmaSpace)\n                }\n            ) {\n                Text(text = i18nState.getString(\"gaussian_bilateral_filter\"), color = Color.Unspecified)\n            }\n        }\n\n        Column(modifier = Modifier.padding(top = 20.dp)) {\n            subTitleWithDivider(text = i18nState.getString(\"mean_shift_filter\"), color = Color.Black)\n\n            Row(modifier = Modifier.padding(top = 10.dp)) {\n                basicTextFieldWithTitle(titleText = \"sp\", spText) { str ->\n                    spText = str\n                }\n\n                basicTextFieldWithTitle(titleText = \"sr\", srText) { str ->\n                    srText = str\n                }\n            }\n\n            Button(\n                modifier = Modifier.align(Alignment.End),\n                onClick = experimentViewClick(state) {\n\n                                val sp = getValidateField(block = { spText.toDouble() } , failed = { \n                                    val errorMsg = i18nState.getString(\"sp_needs_double\")\n                                    showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.MEDIUM, errorMsg, errorMsg)\n                                }) ?: return@experimentViewClick\n            val sr = getValidateField(block = { srText.toDouble() } , failed = { \n                val errorMsg = i18nState.getString(\"sr_needs_double\")\n                showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.MEDIUM, errorMsg, errorMsg)\n            }) ?: return@experimentViewClick\n                    viewModel.pyrMeanShiftFiltering(state, sp, sr)\n                }\n            ) {\n                Text(text = i18nState.getString(\"mean_shift_filter\"), color = Color.Unspecified)\n            }\n        }\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/ai/experiment/ImageEnhanceView.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.ai.experiment\n\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material.Button\nimport androidx.compose.material.Text\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.unit.dp\nimport cn.netdiscovery.monica.state.ApplicationState\nimport cn.netdiscovery.monica.ui.controlpanel.ai.experiment.viewmodel.ImageEnhanceViewModel\nimport cn.netdiscovery.monica.ui.i18n.rememberI18nState\nimport cn.netdiscovery.monica.ui.widget.basicTextFieldWithTitle\nimport cn.netdiscovery.monica.ui.widget.subTitleWithDivider\nimport cn.netdiscovery.monica.ui.widget.title\nimport cn.netdiscovery.monica.utils.getValidateField\nimport cn.netdiscovery.monica.exception.showError\nimport cn.netdiscovery.monica.exception.ErrorType\nimport cn.netdiscovery.monica.exception.ErrorSeverity\nimport org.koin.compose.koinInject\nimport org.slf4j.Logger\nimport org.slf4j.LoggerFactory\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.ai.experiment.ImageEnhanceView\n * @author: Tony Shen\n * @date: 2024/12/3 19:44\n * @version: V1.0 <描述当前版本功能>\n */\nprivate val logger: Logger = LoggerFactory.getLogger(object : Any() {}.javaClass.enclosingClass)\n\n\n@Composable\nfun imageEnhance(state: ApplicationState, title: String) {\n    val i18nState = rememberI18nState()\n    val viewModel: ImageEnhanceViewModel = koinInject()\n\n    var clipLimitText by remember { mutableStateOf(\"4\") }\n    var sizeText by remember { mutableStateOf(\"10\") }\n\n    var gammaText by remember { mutableStateOf(\"1.0\") }\n\n    var amountText by remember { mutableStateOf(\"25\") }\n    var thresholdText by remember { mutableStateOf(\"0\") }\n    var radiusText by remember { mutableStateOf(\"50\") }\n\n    var ratioText by remember { mutableStateOf(\"4\") }\n    var aceRadiusText by remember { mutableStateOf(\"1\") }\n\n    Column (modifier = Modifier.fillMaxSize().padding(start = 20.dp, end =  20.dp, top = 10.dp)) {\n        title(modifier = Modifier.align(Alignment.CenterHorizontally) , text = title, color = Color.Black)\n\n        Column {\n            subTitleWithDivider(text = i18nState.getString(\"histogram_equalization\"), color = Color.Black)\n\n            Button(\n                modifier = Modifier.align(Alignment.End),\n                onClick = experimentViewClick(state) {\n\n                    viewModel.equalizeHist(state)\n                }\n            ) {\n                Text(text = i18nState.getString(\"histogram_equalization_button\"), color = Color.Unspecified)\n            }\n        }\n\n        Column(modifier = Modifier.padding(top = 20.dp)) {\n            subTitleWithDivider(text = i18nState.getString(\"clahe\"), color = Color.Black)\n\n            Row(modifier = Modifier.padding(top = 10.dp)) {\n                basicTextFieldWithTitle(titleText = \"clipLimit\", clipLimitText) { str ->\n                    clipLimitText = str\n                }\n\n                basicTextFieldWithTitle(titleText = \"size\", sizeText) { str ->\n                    sizeText = str\n                }\n            }\n\n            Button(\n                modifier = Modifier.align(Alignment.End),\n                onClick = experimentViewClick(state) {\n\n                    val clipLimit = getValidateField(block = { clipLimitText.toDouble() } , failed = { \n                        val errorMsg = i18nState.getString(\"clip_limit_needs_double\")\n                        showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.MEDIUM, errorMsg, errorMsg)\n                    }) ?: return@experimentViewClick\n                    val size = getValidateField(block = { sizeText.toInt() } , failed = { \n                        val errorMsg = i18nState.getString(\"size_needs_int_for_enhance\")\n                        showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.MEDIUM, errorMsg, errorMsg)\n                    }) ?: return@experimentViewClick\n                    viewModel.clahe(state, clipLimit, size)\n                }\n            ) {\n                Text(text = i18nState.getString(\"clahe_button\"), color = Color.Unspecified)\n            }\n        }\n\n        Column(modifier = Modifier.padding(top = 20.dp)) {\n            subTitleWithDivider(text = i18nState.getString(\"gamma_transform\"), color = Color.Black)\n\n            Row(modifier = Modifier.padding(top = 10.dp)) {\n                basicTextFieldWithTitle(titleText = \"gamma\", gammaText) { str ->\n                    gammaText = str\n                }\n            }\n\n            Button(\n                modifier = Modifier.align(Alignment.End),\n                onClick = experimentViewClick(state) {\n\n                    val gamma = getValidateField(block = { gammaText.toFloat() } , failed = { \n                        val errorMsg = i18nState.getString(\"gamma_needs_float\")\n                        showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.MEDIUM, errorMsg, errorMsg)\n                    }) ?: return@experimentViewClick\n                    viewModel.gammaCorrection(state, gamma)\n                }\n            ) {\n                Text(text = i18nState.getString(\"gamma_transform_button\"), color = Color.Unspecified)\n            }\n        }\n\n        Column(modifier = Modifier.padding(top = 20.dp)) {\n            subTitleWithDivider(text = i18nState.getString(\"laplace_sharpening\"), color = Color.Black)\n\n            Button(\n                modifier = Modifier.align(Alignment.End),\n                onClick = experimentViewClick(state) {\n\n                    viewModel.laplaceSharpening(state)\n                }\n            ) {\n                Text(text = i18nState.getString(\"laplace_sharpen_button\"), color = Color.Unspecified)\n            }\n        }\n\n        Column(modifier = Modifier.padding(top = 20.dp)) {\n            subTitleWithDivider(text = i18nState.getString(\"usm_sharpening\"), color = Color.Black)\n\n            Row(modifier = Modifier.padding(top = 10.dp)) {\n                basicTextFieldWithTitle(titleText = \"Radius\", radiusText) { str ->\n                    radiusText = str\n                }\n\n                basicTextFieldWithTitle(titleText = \"Threshold\", thresholdText) { str ->\n                    thresholdText = str\n                }\n\n                basicTextFieldWithTitle(titleText = \"Amount\", amountText) { str ->\n                    amountText = str\n                }\n            }\n\n            Button(\n                modifier = Modifier.align(Alignment.End),\n                onClick = experimentViewClick(state) {\n\n                    val radius = getValidateField(block = { radiusText.toInt() } , failed = { \n                        val errorMsg = i18nState.getString(\"radius_needs_int\")\n                        showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.MEDIUM, errorMsg, errorMsg)\n                    }) ?: return@experimentViewClick\n                    val threshold = getValidateField(block = { thresholdText.toInt() } , failed = { \n                        val errorMsg = i18nState.getString(\"threshold_needs_int\")\n                        showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.MEDIUM, errorMsg, errorMsg)\n                    }) ?: return@experimentViewClick\n                    val amount = getValidateField(block = { amountText.toInt() } , failed = { \n                        val errorMsg = i18nState.getString(\"amount_needs_int\")\n                        showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.MEDIUM, errorMsg, errorMsg)\n                    }) ?: return@experimentViewClick\n                    viewModel.unsharpMask(state, radius, threshold, amount)\n                }\n            ) {\n                Text(text = i18nState.getString(\"usm_sharpen_button\"), color = Color.Unspecified)\n            }\n        }\n\n        Column(modifier = Modifier.padding(top = 20.dp)) {\n            subTitleWithDivider(text = i18nState.getString(\"automatic_color_balance\"), color = Color.Black)\n\n            Row(modifier = Modifier.padding(top = 10.dp)) {\n                basicTextFieldWithTitle(titleText = \"Ratio\", ratioText) { str ->\n                    ratioText = str\n                }\n\n                basicTextFieldWithTitle(titleText = \"Radius\", aceRadiusText) { str ->\n                    aceRadiusText = str\n                }\n            }\n\n            Button(\n                modifier = Modifier.align(Alignment.End),\n                onClick = experimentViewClick(state) {\n\n                    val ratio = getValidateField(block = { ratioText.toInt() } , failed = { \n                        val errorMsg = i18nState.getString(\"ratio_needs_int\")\n                        showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.MEDIUM, errorMsg, errorMsg)\n                    }) ?: return@experimentViewClick\n                    val radius = getValidateField(block = { aceRadiusText.toInt() } , failed = { \n                        val errorMsg = i18nState.getString(\"radius_needs_int\")\n                        showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.MEDIUM, errorMsg, errorMsg)\n                    }) ?: return@experimentViewClick\n                    viewModel.ace(state, ratio, radius)\n                }\n            ) {\n                Text(text = i18nState.getString(\"auto_color_balance_button\"), color = Color.Unspecified)\n            }\n        }\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/ai/experiment/MatchTemplateView.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.ai.experiment\n\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.*\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.toPainter\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.unit.dp\nimport cn.netdiscovery.monica.imageprocess.BufferedImages\nimport cn.netdiscovery.monica.state.ApplicationState\nimport cn.netdiscovery.monica.domain.MatchTemplateSettings\nimport cn.netdiscovery.monica.ui.i18n.rememberI18nState\nimport cn.netdiscovery.monica.i18n.LocalizationManager\nimport cn.netdiscovery.monica.ui.controlpanel.ai.experiment.viewmodel.MatchTemplateViewModel\nimport cn.netdiscovery.monica.ui.widget.basicTextFieldWithTitle\nimport cn.netdiscovery.monica.ui.widget.subTitleWithDivider\nimport cn.netdiscovery.monica.ui.widget.title\nimport cn.netdiscovery.monica.ui.widget.toolTipButton\nimport cn.netdiscovery.monica.utils.chooseImage\nimport cn.netdiscovery.monica.utils.getBufferedImage\nimport cn.netdiscovery.monica.utils.getValidateField\nimport cn.netdiscovery.monica.exception.showError\nimport cn.netdiscovery.monica.exception.ErrorType\nimport cn.netdiscovery.monica.exception.ErrorSeverity\nimport org.koin.compose.koinInject\nimport org.slf4j.Logger\nimport org.slf4j.LoggerFactory\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.ai.experiment.MatchTemplateView\n * @author: Tony Shen\n * @date: 2024/12/29 14:03\n * @version: V1.0 <描述当前版本功能>\n */\nprivate val logger: Logger = LoggerFactory.getLogger(object : Any() {}.javaClass.enclosingClass)\n\nval matchingMethodTag = arrayListOf(\n    LocalizationManager.getString(\"original_image_matching\"),\n    LocalizationManager.getString(\"grayscale_matching\"),\n    LocalizationManager.getString(\"edge_matching\")\n)\n\nvar matchTemplateSettings: MatchTemplateSettings = MatchTemplateSettings()\n\n@OptIn(ExperimentalMaterialApi::class)\n@Composable\nfun matchTemplate(state: ApplicationState, title: String) {\n    val i18nState = rememberI18nState()\n    val viewModel: MatchTemplateViewModel = koinInject()\n\n    var matchingMethodOption by remember { mutableStateOf(LocalizationManager.getString(\"original_image_matching\")) }\n\n    var angleStartText by remember { mutableStateOf(\"0\") }\n    var angleEndText by remember { mutableStateOf(\"360\") }\n    var angleStepText by remember { mutableStateOf(\"10\") }\n    var scaleStartText by remember { mutableStateOf(\"0.1\") }\n    var scaleEndText by remember { mutableStateOf(\"1.0\") }\n    var scaleStepText by remember { mutableStateOf(\"0.1\") }\n\n    var matchTemplateThresholdText by remember { mutableStateOf(\"0.8\") }\n    var scoreThresholdText by remember { mutableStateOf(\"0.6\") }\n    var nmsThresholdText by remember { mutableStateOf(\"0.3\") }\n\n    Column (modifier = Modifier.fillMaxSize().padding(start = 20.dp, end =  20.dp, top = 10.dp)) {\n        title(modifier = Modifier.align(Alignment.CenterHorizontally), text = title, color = Color.Black)\n\n        Column {\n            subTitleWithDivider(text = i18nState.getString(\"template\"), color = Color.Black)\n\n            Row {\n                Text(modifier = Modifier.width(100.dp).padding(top = 10.dp), text = i18nState.getString(\"import_template\"), color = Color.Unspecified)\n\n                Card(\n                    modifier = Modifier.padding(10.dp).width(150.dp).height(150.dp),\n                    shape = RoundedCornerShape(8.dp),\n                    elevation = 4.dp,\n                    onClick = {\n                        chooseImage(state) { file ->\n                            CVState.templateImage = getBufferedImage(file)\n                        }\n                    },\n                    enabled = CVState.templateImage == null\n                ) {\n                    if (CVState.templateImage == null) {\n                        Text(\n                            modifier = Modifier.fillMaxSize().wrapContentSize(Alignment.Center),\n                            text = i18nState.getString(\"click_to_select_image\"),\n                            textAlign = TextAlign.Center\n                        )\n                    } else {\n                        Box {\n                            Column(\n                                modifier = Modifier.fillMaxSize(),\n                                verticalArrangement = Arrangement.Center,\n                                horizontalAlignment = Alignment.CenterHorizontally\n                            ) {\n                                Image(\n                                    painter = CVState.templateImage!!.toPainter(),\n                                    contentDescription = null,\n                                    contentScale = ContentScale.Fit,\n                                    modifier = Modifier)\n                            }\n\n                            Row(modifier = Modifier.align(Alignment.TopEnd)) {\n                                toolTipButton(text = i18nState.getString(\"delete_source_image\"),\n                                    painter = painterResource(\"images/preview/delete.png\"),\n                                    buttonModifier = Modifier,\n                                    iconModifier = Modifier.size(20.dp),\n                                    onClick = {\n                                        viewModel.clearTemplateImage()\n                                    })\n                            }\n                        }\n                    }\n                }\n            }\n        }\n\n        Column(modifier = Modifier.padding(top = 20.dp)) {\n            subTitleWithDivider(text = i18nState.getString(\"matching_method\"), color = Color.Black)\n\n            Row {\n                matchingMethodTag.forEach {\n\n                    RadioButton(\n                        selected = (it == matchingMethodOption),\n                        onClick = {\n                            matchingMethodOption = it\n                            val index = matchingMethodTag.indexOf(it)\n                            matchTemplateSettings = matchTemplateSettings.copy(matchType = index)\n                        }\n                    )\n                    Text(text = it, modifier = Modifier.width(120.dp).align(Alignment.CenterVertically))\n                }\n            }\n\n        }\n\n        Column(modifier = Modifier.padding(top = 20.dp)) {\n            subTitleWithDivider(text = i18nState.getString(\"rotation\"), color = Color.Black)\n\n            Row(modifier = Modifier.padding(top = 20.dp, bottom = 20.dp)) {\n                basicTextFieldWithTitle(titleText = i18nState.getString(\"min_angle\"), angleStartText) { str ->\n                    angleStartText = str\n                }\n\n                basicTextFieldWithTitle(titleText = i18nState.getString(\"max_angle\"), angleEndText) { str ->\n                    angleEndText = str\n                }\n\n                basicTextFieldWithTitle(titleText = i18nState.getString(\"angle_step\"), angleStepText) { str ->\n                    angleStepText = str\n                }\n            }\n        }\n\n        Column(modifier = Modifier.padding(top = 20.dp)) {\n            subTitleWithDivider(text = i18nState.getString(\"scale\"), color = Color.Black)\n\n            Row(modifier = Modifier.padding(top = 20.dp, bottom = 20.dp)) {\n                basicTextFieldWithTitle(titleText = i18nState.getString(\"min_scale\"), scaleStartText) { str ->\n                    scaleStartText = str\n                }\n\n                basicTextFieldWithTitle(titleText = i18nState.getString(\"max_scale\"), scaleEndText) { str ->\n                    scaleEndText = str\n                }\n\n                basicTextFieldWithTitle(titleText = i18nState.getString(\"scale_step\"), scaleStepText) { str ->\n                    scaleStepText = str\n                }\n            }\n        }\n\n        Column(modifier = Modifier.padding(top = 20.dp)) {\n            subTitleWithDivider(text = i18nState.getString(\"template_matching_params\"), color = Color.Black)\n\n            Row(modifier = Modifier.padding(top = 20.dp, bottom = 20.dp)) {\n                basicTextFieldWithTitle(titleText = i18nState.getString(\"threshold\"), matchTemplateThresholdText) { str ->\n                    matchTemplateThresholdText = str\n                }\n            }\n        }\n\n        Column(modifier = Modifier.padding(top = 20.dp)) {\n            subTitleWithDivider(text = i18nState.getString(\"nms_params\"), color = Color.Black)\n\n            Row(modifier = Modifier.padding(top = 20.dp)) {\n                basicTextFieldWithTitle(titleText = i18nState.getString(\"score_threshold\"), scoreThresholdText) { str ->\n                    scoreThresholdText = str\n                }\n\n                basicTextFieldWithTitle(titleText = i18nState.getString(\"nms_threshold\"), nmsThresholdText) { str ->\n                    nmsThresholdText = str\n                }\n            }\n        }\n\n        Button(\n            modifier = Modifier.padding(top = 10.dp).align(Alignment.End),\n            onClick = experimentViewClick(state) {\n\n                if (CVState.templateImage == null) {\n                    val errorMsg = i18nState.getString(\"please_import_template_first\")\n                    showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.MEDIUM, errorMsg, errorMsg)\n                    return@experimentViewClick\n                }\n\n                val angleStart = getValidateField(block = { angleStartText.toInt() } ,\n                    condition = { it in 0..360 },\n                    failed = { \n                        val errorMsg = i18nState.getString(\"angle_start_needs_int\")\n                        showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.MEDIUM, errorMsg, errorMsg)\n                    }) ?: return@experimentViewClick\n                val angleEnd = getValidateField(block = { angleEndText.toInt() } ,\n                    condition = { it in 0..360 },\n                    failed = { \n                        val errorMsg = i18nState.getString(\"angle_end_needs_int\")\n                        showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.MEDIUM, errorMsg, errorMsg)\n                    }) ?: return@experimentViewClick\n                val angleStep = getValidateField(block = { angleStepText.toInt() } ,\n                    condition = { it > 0 },\n                    failed = { \n                        val errorMsg = i18nState.getString(\"angle_step_needs_int\")\n                        showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.MEDIUM, errorMsg, errorMsg)\n                    }) ?: return@experimentViewClick\n\n                val scaleStart = getValidateField(block = { scaleStartText.toDouble() } ,\n                    condition = { it in 0.0..1.0 },\n                    failed = { \n                        val errorMsg = i18nState.getString(\"scale_start_needs_double\")\n                        showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.MEDIUM, errorMsg, errorMsg)\n                    }) ?: return@experimentViewClick\n                val scaleEnd = getValidateField(block = { scaleEndText.toDouble() } ,\n                    condition = { it in 0.0..1.0 },\n                    failed = { \n                        val errorMsg = i18nState.getString(\"scale_end_needs_double\")\n                        showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.MEDIUM, errorMsg, errorMsg)\n                    }) ?: return@experimentViewClick\n                val scaleStep = getValidateField(block = { scaleStepText.toDouble() } ,\n                    condition = { it > 0 },\n                    failed = { \n                        val errorMsg = i18nState.getString(\"scale_step_needs_double\")\n                        showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.MEDIUM, errorMsg, errorMsg)\n                    }) ?: return@experimentViewClick\n\n                val matchTemplateThreshold = getValidateField(block = { matchTemplateThresholdText.toDouble() } ,\n                    condition = { it in 0.0..1.0 },\n                    failed = { \n                        val errorMsg = i18nState.getString(\"match_template_threshold_needs_double\")\n                        showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.MEDIUM, errorMsg, errorMsg)\n                    }) ?: return@experimentViewClick\n                val scoreThreshold = getValidateField(block = { scoreThresholdText.toFloat() } ,\n                    condition = { it in 0.0..1.0 },\n                    failed = { \n                        val errorMsg = i18nState.getString(\"score_threshold_needs_float\")\n                        showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.MEDIUM, errorMsg, errorMsg)\n                    }) ?: return@experimentViewClick\n                val nmsThreshold = getValidateField(block = { nmsThresholdText.toFloat() } ,\n                    condition = { it in 0.0..1.0 },\n                    failed = { \n                        val errorMsg = i18nState.getString(\"nms_threshold_needs_float\")\n                        showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.MEDIUM, errorMsg, errorMsg)\n                    }) ?: return@experimentViewClick\n\n                matchTemplateSettings = matchTemplateSettings.copy(angleStart = angleStart, angleEnd = angleEnd, angleStep = angleStep,\n                    scaleStart = scaleStart, scaleEnd = scaleEnd, scaleStep = scaleStep,\n                    matchTemplateThreshold = matchTemplateThreshold, scoreThreshold = scoreThreshold, nmsThreshold = nmsThreshold)\n\n                viewModel.matchTemplate(state, matchTemplateSettings)\n            }\n        ) {\n            Text(text = i18nState.getString(\"template_matching\"), color = Color.Unspecified)\n        }\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/ai/experiment/MorphologicalOperationsView.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.ai.experiment\n\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.material.Button\nimport androidx.compose.material.RadioButton\nimport androidx.compose.material.Text\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.unit.dp\nimport cn.netdiscovery.monica.state.ApplicationState\nimport cn.netdiscovery.monica.domain.MorphologicalOperationSettings\nimport cn.netdiscovery.monica.ui.i18n.rememberI18nState\nimport cn.netdiscovery.monica.i18n.LocalizationManager\nimport cn.netdiscovery.monica.ui.controlpanel.ai.experiment.viewmodel.MorphologicalOperationsViewModel\nimport cn.netdiscovery.monica.ui.widget.basicTextFieldWithTitle\nimport cn.netdiscovery.monica.ui.widget.subTitleWithDivider\nimport cn.netdiscovery.monica.ui.widget.title\nimport cn.netdiscovery.monica.utils.getValidateField\nimport cn.netdiscovery.monica.exception.showError\nimport cn.netdiscovery.monica.exception.ErrorType\nimport cn.netdiscovery.monica.exception.ErrorSeverity\nimport org.koin.compose.koinInject\nimport org.slf4j.Logger\nimport org.slf4j.LoggerFactory\nimport java.awt.image.BufferedImage\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.ai.experiment.model.MorphologicalOperationsView\n * @author: Tony Shen\n * @date: 2024/12/21 20:16\n * @version: V1.0 <描述当前版本功能>\n */\nprivate val logger: Logger = LoggerFactory.getLogger(object : Any() {}.javaClass.enclosingClass)\n\nval operatingElementsTag = arrayListOf(\n    LocalizationManager.getString(\"erosion\"),\n    LocalizationManager.getString(\"dilation\"),\n    LocalizationManager.getString(\"opening\"),\n    LocalizationManager.getString(\"closing\"),\n    LocalizationManager.getString(\"morphological_gradient\"),\n    LocalizationManager.getString(\"top_hat\"),\n    LocalizationManager.getString(\"black_hat\"),\n    LocalizationManager.getString(\"hit_miss\")\n)\nval structuralElementsTag = arrayListOf(\n    LocalizationManager.getString(\"rectangle\"),\n    LocalizationManager.getString(\"cross\"),\n    LocalizationManager.getString(\"ellipse\")\n)\nval tagList1 = operatingElementsTag.take(4)\nval tagList2 = operatingElementsTag.takeLast(4)\n\nvar morphologicalOperationSettings: MorphologicalOperationSettings = MorphologicalOperationSettings()\n\n@Composable\nfun morphologicalOperations(state: ApplicationState, title: String) {\n    val i18nState = rememberI18nState()\n    val viewModel: MorphologicalOperationsViewModel = koinInject()\n\n    var operatingElementOption by remember { mutableStateOf(\"Null\") }\n    var structuralElementOption by remember { mutableStateOf(LocalizationManager.getString(\"rectangle\")) }\n\n    var widthText by remember { mutableStateOf(\"3\") }\n    var heightText by remember { mutableStateOf(\"3\") }\n\n    Column (modifier = Modifier.fillMaxSize().padding(start = 20.dp, end =  20.dp, top = 10.dp)) {\n        title(modifier = Modifier.align(Alignment.CenterHorizontally), text = title, color = Color.Black)\n\n        Column {\n            subTitleWithDivider(text = i18nState.getString(\"operation_element\"), color = Color.Black)\n\n            Row {\n                tagList1.forEach {\n                    RadioButton(\n                        selected = (it == operatingElementOption),\n                        onClick = {\n                            operatingElementOption = it\n                            val index = operatingElementsTag.indexOf(it)\n                            morphologicalOperationSettings = morphologicalOperationSettings.copy(op = index)\n                        }\n                    )\n                    Text(text = it, modifier = Modifier.width(120.dp).align(Alignment.CenterVertically))\n                }\n            }\n\n            Row {\n                tagList2.forEach {\n\n                    RadioButton(\n                        selected = (it == operatingElementOption),\n                        onClick = {\n                            operatingElementOption = it\n                            val index = operatingElementsTag.indexOf(it)\n                            morphologicalOperationSettings = morphologicalOperationSettings.copy(op = index)\n                        }\n                    )\n                    Text(text = it, modifier = Modifier.width(120.dp).align(Alignment.CenterVertically))\n                }\n            }\n        }\n\n        Column(modifier = Modifier.padding(top = 20.dp)) {\n            subTitleWithDivider(text = i18nState.getString(\"structural_element\"), color = Color.Black)\n\n            Row {\n                structuralElementsTag.forEach {\n\n                    RadioButton(\n                        selected = (it == structuralElementOption),\n                        onClick = {\n                            structuralElementOption = it\n                            val index = structuralElementsTag.indexOf(it)\n                            morphologicalOperationSettings = morphologicalOperationSettings.copy(shape = index)\n                        }\n                    )\n                    Text(text = it, modifier = Modifier.width(120.dp).align(Alignment.CenterVertically))\n                }\n            }\n\n            Row(modifier = Modifier.padding(top = 20.dp)) {\n                Text(modifier = Modifier.width(70.dp), text = i18nState.getString(\"structural_element\") + \"：\", color = Color.Unspecified)\n\n                basicTextFieldWithTitle(titleText = i18nState.getString(\"width\"), widthText) { str ->\n                    widthText = str\n                }\n\n                basicTextFieldWithTitle(titleText = i18nState.getString(\"height\"), heightText) { str ->\n                    heightText = str\n                }\n            }\n        }\n\n        Button(\n            modifier = Modifier.padding(top = 10.dp).align(Alignment.End),\n            onClick = experimentViewClick(state) {\n\n                if(state.currentImage?.type == BufferedImage.TYPE_BYTE_BINARY) {\n                    val width = getValidateField(block = { widthText.toInt() } , failed = { \n                        val errorMsg = i18nState.getString(\"width_needs_int_for_morph\")\n                        showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.MEDIUM, errorMsg, errorMsg)\n                    }) ?: return@experimentViewClick\n                    val height = getValidateField(block = { heightText.toInt() } , failed = { \n                        val errorMsg = i18nState.getString(\"height_needs_int_for_morph\")\n                        showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.MEDIUM, errorMsg, errorMsg)\n                    }) ?: return@experimentViewClick\n\n                    morphologicalOperationSettings = morphologicalOperationSettings.copy(width = width, height = height)\n\n                    viewModel.morphologyEx(state, morphologicalOperationSettings)\n                } else {\n                    val errorMsg = i18nState.getString(\"please_binarize_image_first\")\n                    showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.MEDIUM, errorMsg, errorMsg)\n                }\n            }\n        ) {\n            Text(text = i18nState.getString(\"morphological_operations\"), color = Color.Unspecified)\n        }\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/ai/experiment/NavController.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.ai.experiment\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.MutableState\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.saveable.rememberSaveable\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.ai.experiment.NavController\n * @author: Tony Shen\n * @date: 2024/9/23 20:04\n * @version: V1.0 <描述当前版本功能>\n */\nclass NavController(\n    private val startDestination: String,\n    private var backStackScreens: MutableSet<String> = mutableSetOf()\n) {\n    // Variable to store the state of the current screen\n    var currentScreen: MutableState<String> = mutableStateOf(startDestination)\n\n    // Function to handle the navigation between the screen\n    fun navigate(route: String) {\n        if (route != currentScreen.value) {\n            if (backStackScreens.contains(currentScreen.value) && currentScreen.value != startDestination) {\n                backStackScreens.remove(currentScreen.value)\n            }\n\n            if (route == startDestination) {\n                backStackScreens = mutableSetOf()\n            } else {\n                backStackScreens.add(currentScreen.value)\n            }\n\n            currentScreen.value = route\n        }\n    }\n\n    // Function to handle the back\n    fun navigateBack() {\n        if (backStackScreens.isNotEmpty()) {\n            currentScreen.value = backStackScreens.last()\n            backStackScreens.remove(currentScreen.value)\n        }\n    }\n}\n\n/**\n * Composable to remember the state of the navcontroller\n */\n@Composable\nfun rememberNavController(\n    startDestination: String,\n    backStackScreens: MutableSet<String> = mutableSetOf()\n): MutableState<NavController> = rememberSaveable {\n    mutableStateOf(NavController(startDestination, backStackScreens))\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/ai/experiment/NavigationHost.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.ai.experiment\n\nimport androidx.compose.runtime.Composable\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.ai.experiment.NavigationHost\n * @author: Tony Shen\n * @date: 2024/9/23 20:06\n * @version: V1.0 <描述当前版本功能>\n */\nclass NavigationHost(\n    val navController: NavController,\n    val contents: @Composable NavigationGraphBuilder.() -> Unit\n) {\n\n    @Composable\n    fun build() {\n        NavigationGraphBuilder().renderContents()\n    }\n\n    inner class NavigationGraphBuilder(\n        val navController: NavController = this@NavigationHost.navController\n    ) {\n        @Composable\n        fun renderContents() {\n            this@NavigationHost.contents(this)\n        }\n    }\n}\n\n\n/**\n * Composable to build the Navigation Host\n */\n@Composable\nfun NavigationHost.NavigationGraphBuilder.composable(\n    route: String,\n    content: @Composable () -> Unit\n) {\n    if (navController.currentScreen.value == route) {\n        content()\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/ai/experiment/viewmodel/BinaryImageViewModel.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.ai.experiment.viewmodel\n\nimport cn.netdiscovery.monica.config.MODULE_OPENCV\nimport cn.netdiscovery.monica.history.EditHistoryCenter\nimport cn.netdiscovery.monica.history.modules.opencv.CVParams\nimport cn.netdiscovery.monica.history.modules.opencv.recordCVOperation\nimport cn.netdiscovery.monica.manager.OpenCVManager\nimport cn.netdiscovery.monica.opencv.ImageProcess\nimport cn.netdiscovery.monica.state.ApplicationState\nimport cn.netdiscovery.monica.utils.extensions.launchWithLoading\nimport cn.netdiscovery.monica.utils.logger\nimport org.slf4j.Logger\nimport java.awt.image.BufferedImage\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.ai.experiment.viewmodel.BinaryImageAnalysisViewModel\n * @author: Tony Shen\n * @date: 2024/10/7 16:07\n * @version: V1.0 <描述当前版本功能>\n */\nclass BinaryImageViewModel {\n    private val logger: Logger = logger<BinaryImageViewModel>()\n    private val manager = EditHistoryCenter.getManager<CVParams>(MODULE_OPENCV)\n\n    fun cvtGray(state: ApplicationState) {\n\n        state.scope.launchWithLoading {\n            OpenCVManager.invokeCV(state, type = BufferedImage.TYPE_BYTE_GRAY, action = { byteArray ->\n\n                manager.recordCVOperation(operation = \"cvtGray\", description = \"灰度化\") {}\n\n                ImageProcess.cvtGray(byteArray)\n            }, failure = { e ->\n                logger.error(\"cvtGray is failed\", e)\n            })\n        }\n    }\n\n    fun threshold(state: ApplicationState, typeSelected: String, thresholdSelected: String) {\n\n        val thresholdType1 = when(typeSelected) {\n            \"THRESH_BINARY\" -> 0\n            \"THRESH_BINARY_INV\" -> 1\n            else -> 0\n        }\n\n        val thresholdType2 = when(thresholdSelected) {\n            \"THRESH_OTSU\" -> 8\n            \"THRESH_TRIANGLE\" -> 16\n            else -> 8\n        }\n\n        state.scope.launchWithLoading {\n            OpenCVManager.invokeCV(state, type = BufferedImage.TYPE_BYTE_BINARY, action = { byteArray ->\n\n                manager.recordCVOperation(operation = \"threshold\", description = \"阈值分割\") {\n                    this.parameters[\"thresholdType1\"] = thresholdType1\n                    this.parameters[\"thresholdType2\"] = thresholdType2\n                }\n\n                ImageProcess.threshold(byteArray, thresholdType1, thresholdType2)\n            }, failure = { e ->\n                logger.error(\"threshold is failed\", e)\n            })\n        }\n    }\n\n    fun adaptiveThreshold(state: ApplicationState, adaptiveMethodSelected: String, typeSelected: String, blockSize:Int, c:Int) {\n\n        val adaptiveMethod = when(adaptiveMethodSelected) {\n            \"ADAPTIVE_THRESH_MEAN_C\" -> 0\n            \"ADAPTIVE_THRESH_GAUSSIAN_C\" -> 1\n            else -> 0\n        }\n\n        val thresholdType = when(typeSelected) {\n            \"THRESH_BINARY\" -> 0\n            \"THRESH_BINARY_INV\" -> 1\n            else -> 0\n        }\n\n        state.scope.launchWithLoading {\n            OpenCVManager.invokeCV(state, type = BufferedImage.TYPE_BYTE_BINARY, action = { byteArray ->\n\n                manager.recordCVOperation(operation = \"adaptiveThreshold\", description = \"自适应阈值分割\") {\n                    this.parameters[\"adaptiveMethod\"] = adaptiveMethod\n                    this.parameters[\"thresholdType\"] = thresholdType\n                }\n\n                ImageProcess.adaptiveThreshold(byteArray, adaptiveMethod, thresholdType, blockSize, c)\n            }, failure = { e ->\n                logger.error(\"adaptiveThreshold is failed\", e)\n            })\n        }\n    }\n\n    fun inRange(state: ApplicationState, hmin:Int, smin:Int, vmin:Int, hmax:Int, smax:Int, vmax:Int) {\n        state.scope.launchWithLoading {\n            OpenCVManager.invokeCV(state, type = BufferedImage.TYPE_BYTE_BINARY, action = { byteArray ->\n\n                manager.recordCVOperation(operation = \"inRange\", description = \"颜色分割\") {\n                    this.parameters[\"hmin\"]=hmin\n                    this.parameters[\"smin\"]=smin\n                    this.parameters[\"vmin\"]=vmin\n                    this.parameters[\"hmax\"]=hmax\n                    this.parameters[\"smax\"]=smax\n                    this.parameters[\"vmax\"]=vmax\n                }\n\n                ImageProcess.inRange(byteArray, hmin, smin, vmin, hmax, smax, vmax)\n            }, failure = { e ->\n                logger.error(\"inRange is failed\", e)\n            })\n        }\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/ai/experiment/viewmodel/ContourAnalysisViewModel.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.ai.experiment.viewmodel\n\nimport cn.netdiscovery.monica.config.MODULE_OPENCV\nimport cn.netdiscovery.monica.manager.OpenCVManager\nimport cn.netdiscovery.monica.state.ApplicationState\nimport cn.netdiscovery.monica.domain.ContourDisplaySettings\nimport cn.netdiscovery.monica.domain.ContourFilterSettings\nimport cn.netdiscovery.monica.history.EditHistoryCenter\nimport cn.netdiscovery.monica.history.modules.opencv.CVParams\nimport cn.netdiscovery.monica.history.modules.opencv.recordCVOperation\nimport cn.netdiscovery.monica.imageprocess.utils.extension.image2ByteArray\nimport cn.netdiscovery.monica.opencv.ImageProcess\nimport cn.netdiscovery.monica.utils.extensions.launchWithLoading\nimport cn.netdiscovery.monica.utils.logger\nimport com.safframework.rxcache.utils.GsonUtils\nimport org.slf4j.Logger\nimport java.awt.image.BufferedImage\nimport kotlin.collections.set\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.ai.experiment.viewmodel.ContourAnalysisViewModel\n * @author: Tony Shen\n * @date: 2024/10/26 13:54\n * @version: V1.0 <描述当前版本功能>\n */\nclass ContourAnalysisViewModel {\n    private val logger: Logger = logger<ContourAnalysisViewModel>()\n    private val manager = EditHistoryCenter.getManager<CVParams>(MODULE_OPENCV)\n\n    fun contourAnalysis(state: ApplicationState, contourFilterSettings: ContourFilterSettings, contourDisplaySettings: ContourDisplaySettings) {\n\n        logger.info(\"contourFilterSettings = ${GsonUtils.toJson(contourFilterSettings)}\")\n        logger.info(\"contourDisplaySettings = ${GsonUtils.toJson(contourDisplaySettings)}\")\n\n        val type = if (contourDisplaySettings.showOriginalImage) { BufferedImage.TYPE_INT_ARGB } else BufferedImage.TYPE_BYTE_BINARY\n\n        state.scope.launchWithLoading {\n            OpenCVManager.invokeCV(state, type = type, action = { byteArray ->\n                val srcByteArray = state.rawImage!!.image2ByteArray()\n\n                manager.recordCVOperation(operation = \"contourAnalysis\", description = \"轮廓分析\") {\n                    this.parameters[\"contourFilterSettings\"] = contourFilterSettings\n                    this.parameters[\"contourDisplaySettings\"] = contourDisplaySettings\n                }\n\n                val scalar = state.toOutputBoxScalar()\n\n                ImageProcess.contourAnalysis(srcByteArray, byteArray, scalar, contourFilterSettings, contourDisplaySettings)\n            }, failure = { e ->\n                logger.error(\"contourAnalysis is failed\", e)\n            })\n        }\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/ai/experiment/viewmodel/EdgeDetectionViewModel.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.ai.experiment.viewmodel\n\nimport cn.netdiscovery.monica.config.MODULE_OPENCV\nimport cn.netdiscovery.monica.history.EditHistoryCenter\nimport cn.netdiscovery.monica.history.modules.opencv.CVParams\nimport cn.netdiscovery.monica.history.modules.opencv.recordCVOperation\nimport cn.netdiscovery.monica.opencv.ImageProcess\nimport cn.netdiscovery.monica.manager.OpenCVManager\nimport cn.netdiscovery.monica.state.ApplicationState\nimport cn.netdiscovery.monica.utils.extensions.launchWithLoading\nimport cn.netdiscovery.monica.utils.logger\nimport org.slf4j.Logger\nimport java.awt.image.BufferedImage\nimport kotlin.collections.set\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.ai.experiment.viewmodel.EdgeDetectionViewModel\n * @author: Tony Shen\n * @date:  2024/10/13 22:23\n * @version: V1.0 <描述当前版本功能>\n */\nclass EdgeDetectionViewModel {\n    private val logger: Logger = logger<EdgeDetectionViewModel>()\n    private val manager = EditHistoryCenter.getManager<CVParams>(MODULE_OPENCV)\n\n    fun roberts(state: ApplicationState) {\n\n        state.scope.launchWithLoading {\n            OpenCVManager.invokeCV(state, type = BufferedImage.TYPE_BYTE_GRAY, action = { byteArray ->\n\n                manager.recordCVOperation(operation = \"roberts\", description = \"实现 roberts 算子\") {}\n\n                ImageProcess.roberts(byteArray)\n            }, failure = { e ->\n                logger.error(\"roberts is failed\", e)\n            })\n        }\n    }\n\n    fun prewitt(state: ApplicationState) {\n\n        state.scope.launchWithLoading {\n            OpenCVManager.invokeCV(state, type = BufferedImage.TYPE_BYTE_GRAY, action = { byteArray ->\n\n                manager.recordCVOperation(operation = \"prewitt\", description = \"实现 prewitt 算子\") {}\n\n                ImageProcess.prewitt(byteArray)\n            }, failure = { e ->\n                logger.error(\"prewitt is failed\", e)\n            })\n        }\n    }\n\n    fun sobel(state: ApplicationState) {\n\n        state.scope.launchWithLoading {\n            OpenCVManager.invokeCV(state, type = BufferedImage.TYPE_BYTE_GRAY, action = { byteArray ->\n\n                manager.recordCVOperation(operation = \"sobel\", description = \"实现 sobel 算子\") {}\n\n                ImageProcess.sobel(byteArray)\n            }, failure = { e ->\n                logger.error(\"sobel is failed\", e)\n            })\n        }\n    }\n\n    fun laplace(state: ApplicationState) {\n\n        state.scope.launchWithLoading {\n            OpenCVManager.invokeCV(state, type = BufferedImage.TYPE_BYTE_GRAY, action = { byteArray ->\n\n                manager.recordCVOperation(operation = \"laplace\", description = \"实现 laplace 算子\") {}\n\n                ImageProcess.laplace(byteArray)\n            }, failure = { e ->\n                logger.error(\"laplace is failed\", e)\n            })\n        }\n    }\n\n    fun log(state: ApplicationState) {\n\n        state.scope.launchWithLoading {\n            OpenCVManager.invokeCV(state, type = BufferedImage.TYPE_BYTE_GRAY, action = { byteArray ->\n\n                manager.recordCVOperation(operation = \"log\", description = \"实现 LoG 算子\") {}\n\n                ImageProcess.log(byteArray)\n            }, failure = { e ->\n                logger.error(\"log is failed\", e)\n            })\n        }\n    }\n\n    fun dog(state: ApplicationState, sigma1:Double, sigma2: Double, size:Int) {\n\n        state.scope.launchWithLoading {\n            OpenCVManager.invokeCV(state, type = BufferedImage.TYPE_BYTE_GRAY, action = { byteArray ->\n\n                manager.recordCVOperation(operation = \"dog\", description = \"实现 DoG 算子\") {\n                    this.parameters[\"sigma1\"] = sigma1\n                    this.parameters[\"sigma2\"] = sigma2\n                    this.parameters[\"size\"] = size\n                }\n\n                ImageProcess.dog(byteArray, sigma1, sigma2, size)\n            }, failure = { e ->\n                logger.error(\"log is failed\", e)\n            })\n        }\n    }\n\n    fun canny(state: ApplicationState, threshold1:Double, threshold2: Double, apertureSize:Int) {\n\n        state.scope.launchWithLoading {\n            OpenCVManager.invokeCV(state, type = BufferedImage.TYPE_BYTE_BINARY, action = { byteArray ->\n\n                manager.recordCVOperation(operation = \"canny\", description = \"实现 canny 算子\") {\n                    this.parameters[\"threshold1\"] = threshold1\n                    this.parameters[\"threshold2\"] = threshold2\n                    this.parameters[\"apertureSize\"] = apertureSize\n                }\n\n                ImageProcess.canny(byteArray,threshold1,threshold2,apertureSize)\n            }, failure = { e ->\n                logger.error(\"canny is failed\", e)\n            })\n        }\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/ai/experiment/viewmodel/HistoryViewModel.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.ai.experiment.viewmodel\n\nimport cn.netdiscovery.monica.config.MODULE_OPENCV\nimport cn.netdiscovery.monica.history.EditHistoryCenter\nimport cn.netdiscovery.monica.history.HistoryEntry\nimport cn.netdiscovery.monica.history.modules.opencv.CVParams\nimport cn.netdiscovery.monica.utils.logger\nimport org.slf4j.Logger\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.ai.experiment.viewmodel.HistoryViewModel\n * @author: Tony Shen\n * @date: 2025/7/30 09:52\n * @version: V1.0 <描述当前版本功能>\n */\nclass HistoryViewModel {\n    private val logger: Logger = logger<HistoryViewModel>()\n    private val manager = EditHistoryCenter.getManager<CVParams>(MODULE_OPENCV)\n\n    fun getOperationLog():List<HistoryEntry>{\n\n        return manager.getOperationLog().asReversed()\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/ai/experiment/viewmodel/ImageDenoisingViewModel.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.ai.experiment.viewmodel\n\nimport cn.netdiscovery.monica.config.MODULE_OPENCV\nimport cn.netdiscovery.monica.history.EditHistoryCenter\nimport cn.netdiscovery.monica.history.modules.opencv.CVParams\nimport cn.netdiscovery.monica.history.modules.opencv.recordCVOperation\nimport cn.netdiscovery.monica.opencv.ImageProcess\nimport cn.netdiscovery.monica.manager.OpenCVManager\nimport cn.netdiscovery.monica.state.ApplicationState\nimport cn.netdiscovery.monica.utils.extensions.launchWithLoading\nimport cn.netdiscovery.monica.utils.logger\nimport org.slf4j.Logger\nimport kotlin.collections.set\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.ai.experiment.viewmodel.ImageDenoisingViewModel\n * @author: Tony Shen\n * @date: 2024/12/4 15:44\n * @version: V1.0 <描述当前版本功能>\n */\nclass ImageDenoisingViewModel {\n    private val logger: Logger = logger<ImageDenoisingViewModel>()\n    private val manager = EditHistoryCenter.getManager<CVParams>(MODULE_OPENCV)\n\n    fun gaussianBlur(state: ApplicationState, ksize:Int, sigmaX: Double = 0.0, sigmaY: Double = 0.0) {\n\n        state.scope.launchWithLoading {\n            OpenCVManager.invokeCV(state, action = { byteArray ->\n\n                manager.recordCVOperation(operation = \"gaussianBlur\", description = \"实现高斯滤波\") {\n                    this.parameters[\"ksize\"] = ksize\n                    this.parameters[\"sigmaX\"] = sigmaX\n                    this.parameters[\"sigmaY\"] = sigmaY\n                }\n\n                ImageProcess.gaussianBlur(byteArray, ksize, sigmaX, sigmaY)\n            }, failure = { e ->\n                logger.error(\"gaussianBlur is failed\", e)\n            })\n        }\n    }\n\n    fun medianBlur(state: ApplicationState, ksize:Int) {\n\n        state.scope.launchWithLoading {\n            OpenCVManager.invokeCV(state, action = { byteArray ->\n\n                manager.recordCVOperation(operation = \"medianBlur\", description = \"实现中值滤波\") {\n                    this.parameters[\"ksize\"] = ksize\n                }\n\n                ImageProcess.medianBlur(byteArray, ksize)\n            }, failure = { e ->\n                logger.error(\"medianBlur is failed\", e)\n            })\n        }\n    }\n\n    fun bilateralFilter(state: ApplicationState, d:Int, sigmaColor:Double, sigmaSpace:Double) {\n\n        state.scope.launchWithLoading {\n            OpenCVManager.invokeCV(state, action = { byteArray ->\n\n                manager.recordCVOperation(operation = \"bilateralFilter\", description = \"实现高斯双边滤波\") {\n                    this.parameters[\"d\"] = d\n                    this.parameters[\"sigmaColor\"] = sigmaColor\n                    this.parameters[\"sigmaSpace\"] = sigmaSpace\n                }\n\n                ImageProcess.bilateralFilter(byteArray, d, sigmaColor, sigmaSpace)\n            }, failure = { e ->\n                logger.error(\"medianBlur is failed\", e)\n            })\n        }\n    }\n\n    fun pyrMeanShiftFiltering(state: ApplicationState, sp: Double, sr: Double) {\n\n        state.scope.launchWithLoading {\n            OpenCVManager.invokeCV(state, action = { byteArray ->\n\n                manager.recordCVOperation(operation = \"pyrMeanShiftFiltering\", description = \"实现均值迁移滤波\") {\n                    this.parameters[\"sp\"] = sp\n                    this.parameters[\"sr\"] = sr\n                }\n\n                ImageProcess.pyrMeanShiftFiltering(byteArray, sp, sr)\n            }, failure = { e ->\n                logger.error(\"medianBlur is failed\", e)\n            })\n        }\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/ai/experiment/viewmodel/ImageEnhanceViewModel.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.ai.experiment.viewmodel\n\nimport cn.netdiscovery.monica.config.MODULE_OPENCV\nimport cn.netdiscovery.monica.history.EditHistoryCenter\nimport cn.netdiscovery.monica.history.modules.opencv.CVParams\nimport cn.netdiscovery.monica.history.modules.opencv.recordCVOperation\nimport cn.netdiscovery.monica.opencv.ImageProcess\nimport cn.netdiscovery.monica.manager.OpenCVManager\nimport cn.netdiscovery.monica.state.ApplicationState\nimport cn.netdiscovery.monica.utils.extensions.launchWithLoading\nimport cn.netdiscovery.monica.utils.logger\nimport org.slf4j.Logger\nimport kotlin.collections.set\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.ai.experiment.viewmodel.ImageEnhanceViewModel\n * @author: Tony Shen\n * @date: 2024/7/17 21:33\n * @version: V1.0 <描述当前版本功能>\n */\nclass ImageEnhanceViewModel {\n    private val logger: Logger = logger<ImageEnhanceViewModel>()\n    private val manager = EditHistoryCenter.getManager<CVParams>(MODULE_OPENCV)\n\n    fun equalizeHist(state: ApplicationState) {\n        state.scope.launchWithLoading {\n            OpenCVManager.invokeCV(state, action = { byteArray ->\n\n                manager.recordCVOperation(operation = \"equalizeHist\", description = \"直方图均衡化\") {}\n\n                ImageProcess.equalizeHist(byteArray)\n            }, failure = { e ->\n                logger.error(\"equalizeHist is failed\", e)\n            })\n        }\n    }\n\n    fun clahe(state: ApplicationState, clipLimit:Double, size:Int) {\n        state.scope.launchWithLoading {\n            OpenCVManager.invokeCV(state, action = { byteArray ->\n\n                manager.recordCVOperation(operation = \"clahe\", description = \"限制对比度自适应直方图均衡\") {\n                    this.parameters[\"clipLimit\"] = clipLimit\n                    this.parameters[\"size\"] = size\n                }\n\n                ImageProcess.clahe(byteArray, clipLimit, size)\n            }, failure = { e ->\n                logger.error(\"clahe is failed\", e)\n            })\n        }\n    }\n\n    fun gammaCorrection(state: ApplicationState, gamma:Float) {\n        state.scope.launchWithLoading {\n            OpenCVManager.invokeCV(state, action = { byteArray ->\n\n                manager.recordCVOperation(operation = \"gammaCorrection\", description = \"gamma 校正\") {\n                    this.parameters[\"gamma\"] = gamma\n                }\n\n                ImageProcess.gammaCorrection(byteArray, gamma)\n            }, failure = { e ->\n                logger.error(\"gammaCorrection is failed\", e)\n            })\n        }\n    }\n\n    fun laplaceSharpening(state: ApplicationState) {\n        state.scope.launchWithLoading {\n            OpenCVManager.invokeCV(state, action = { byteArray ->\n\n                manager.recordCVOperation(operation = \"laplaceSharpening\", description = \"laplace 锐化\") {}\n\n                ImageProcess.laplaceSharpening(byteArray)\n            }, failure = { e ->\n                logger.error(\"laplace is failed\", e)\n            })\n        }\n    }\n\n    fun unsharpMask(state: ApplicationState,radius:Int,threshold:Int,amount:Int) {\n        state.scope.launchWithLoading {\n            OpenCVManager.invokeCV(state, action = { byteArray ->\n\n                manager.recordCVOperation(operation = \"unsharpMask\", description = \"USM 锐化\") {\n                    this.parameters[\"radius\"] = radius\n                    this.parameters[\"threshold\"] = threshold\n                    this.parameters[\"amount\"] = amount\n                }\n\n                ImageProcess.unsharpMask(byteArray,radius,threshold,amount)\n            }, failure = { e ->\n                logger.error(\"unsharpMask is failed\", e)\n            })\n        }\n    }\n\n    fun ace(state: ApplicationState, ratio:Int, radius:Int) {\n        state.scope.launchWithLoading {\n            OpenCVManager.invokeCV(state, action = { byteArray ->\n\n                manager.recordCVOperation(operation = \"ace\", description = \"自动色彩均衡\") {\n                    this.parameters[\"ratio\"] = ratio\n                    this.parameters[\"radius\"] = radius\n                }\n\n                ImageProcess.ace(byteArray,ratio,radius)\n            }, failure = { e ->\n                logger.error(\"ace is failed\", e)\n            })\n        }\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/ai/experiment/viewmodel/MatchTemplateViewModel.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.ai.experiment.viewmodel\n\nimport cn.netdiscovery.monica.config.MODULE_OPENCV\nimport cn.netdiscovery.monica.domain.MatchTemplateSettings\nimport cn.netdiscovery.monica.history.EditHistoryCenter\nimport cn.netdiscovery.monica.history.modules.opencv.CVParams\nimport cn.netdiscovery.monica.history.modules.opencv.recordCVOperation\nimport cn.netdiscovery.monica.imageprocess.utils.extension.image2ByteArray\nimport cn.netdiscovery.monica.opencv.ImageProcess\nimport cn.netdiscovery.monica.manager.OpenCVManager\nimport cn.netdiscovery.monica.state.ApplicationState\nimport cn.netdiscovery.monica.ui.controlpanel.ai.experiment.CVState\nimport cn.netdiscovery.monica.utils.extensions.launchWithLoading\nimport cn.netdiscovery.monica.utils.logger\nimport com.safframework.rxcache.utils.GsonUtils\nimport org.slf4j.Logger\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.ai.experiment.viewmodel.MatchTemplateViewModel\n * @author: Tony Shen\n * @date: 2025/1/1 15:32\n * @version: V1.0 <描述当前版本功能>\n */\nclass MatchTemplateViewModel {\n\n    private val logger: Logger = logger<MatchTemplateViewModel>()\n    private val manager = EditHistoryCenter.getManager<CVParams>(MODULE_OPENCV)\n\n    fun clearTemplateImage() {\n        if (CVState.templateImage!=null) {\n            CVState.templateImage = null\n        }\n    }\n\n    fun matchTemplate(state: ApplicationState, matchTemplateSettings: MatchTemplateSettings) {\n        logger.info(\"matchTemplateSettings = ${GsonUtils.toJson(matchTemplateSettings)}\")\n\n        if (CVState.templateImage != null) {\n            state.scope.launchWithLoading {\n                OpenCVManager.invokeCV(state, action = { byteArray ->\n                    val templateByteArray = CVState.templateImage!!.image2ByteArray()\n                    val scalar = state.toOutputBoxScalar()\n\n                    manager.recordCVOperation(operation = \"matchTemplate\", description = \"模版匹配\") {\n                        this.parameters[\"matchTemplateSettings\"] = matchTemplateSettings\n                    }\n\n                    ImageProcess.matchTemplate(byteArray, templateByteArray, scalar, matchTemplateSettings)\n                }, failure = { e ->\n                    logger.error(\"contourAnalysis is failed\", e)\n                })\n            }\n        }\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/ai/experiment/viewmodel/MorphologicalOperationsViewModel.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.ai.experiment.viewmodel\n\nimport cn.netdiscovery.monica.config.MODULE_OPENCV\nimport cn.netdiscovery.monica.manager.OpenCVManager\nimport cn.netdiscovery.monica.state.ApplicationState\nimport cn.netdiscovery.monica.domain.MorphologicalOperationSettings\nimport cn.netdiscovery.monica.history.EditHistoryCenter\nimport cn.netdiscovery.monica.history.modules.opencv.CVParams\nimport cn.netdiscovery.monica.history.modules.opencv.recordCVOperation\nimport cn.netdiscovery.monica.opencv.ImageProcess\nimport cn.netdiscovery.monica.utils.extensions.launchWithLoading\nimport cn.netdiscovery.monica.utils.logger\nimport com.safframework.rxcache.utils.GsonUtils\nimport org.slf4j.Logger\nimport java.awt.image.BufferedImage\nimport kotlin.collections.set\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.ai.experiment.viewmodel.MorphologicalOperationsViewModel\n * @author: Tony Shen\n * @date: 2024/12/26 20:21\n * @version: V1.0 <描述当前版本功能>\n */\nclass MorphologicalOperationsViewModel {\n    private val logger: Logger = logger<MorphologicalOperationsViewModel>()\n    private val manager = EditHistoryCenter.getManager<CVParams>(MODULE_OPENCV)\n\n    fun morphologyEx(state: ApplicationState, morphologicalOperationSettings: MorphologicalOperationSettings) {\n\n        logger.info(\"morphologicalOperationSettings = ${GsonUtils.toJson(morphologicalOperationSettings)}\")\n\n        state.scope.launchWithLoading {\n            OpenCVManager.invokeCV(state, type = BufferedImage.TYPE_BYTE_BINARY, action = { byteArray ->\n\n                manager.recordCVOperation(operation = \"morphologyEx\", description = \"形态学操作\") {\n                    this.parameters[\"morphologicalOperationSettings\"] = morphologicalOperationSettings\n                }\n\n                ImageProcess.morphologyEx(byteArray, morphologicalOperationSettings)\n            }, failure = { e ->\n                logger.error(\"contourAnalysis is failed\", e)\n            })\n        }\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/ai/faceswap/FaceSwapView.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.ai.faceswap\n\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.*\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.toPainter\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport cn.netdiscovery.monica.state.ApplicationState\nimport cn.netdiscovery.monica.ui.i18n.rememberI18nState\nimport cn.netdiscovery.monica.ui.widget.*\nimport cn.netdiscovery.monica.utils.chooseImage\nimport cn.netdiscovery.monica.utils.getBufferedImage\nimport loadingDisplay\nimport org.koin.compose.koinInject\nimport org.slf4j.Logger\nimport org.slf4j.LoggerFactory\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.ai.faceswap.FaceSwapView\n * @author: Tony Shen\n * @date: 2024/8/25 13:02\n * @version: V1.0 <描述当前版本功能>\n */\nprivate val logger: Logger = LoggerFactory.getLogger(object : Any() {}.javaClass.enclosingClass)\n\nprivate var showToast by mutableStateOf(false)\nprivate var toastMessage by mutableStateOf(\"\")\n\n@OptIn(ExperimentalMaterialApi::class)\n@Composable\nfun faceSwap(state: ApplicationState) {\n    val i18nState = rememberI18nState()\n    val viewModel: FaceSwapViewModel = koinInject()\n\n    val showSwapFaceSettings = remember { mutableStateOf(false) }\n    val selectedOption = remember { mutableStateOf(false) }\n\n    PageLifecycle(\n        onInit = {\n            logger.info(\"FaceSwapView 启动时初始化\")\n        },\n        onDisposeEffect = {\n            logger.info(\"FaceSwapView 关闭时释放资源\")\n            viewModel.clearTargetImage()\n        }\n    )\n\n    Box(\n        Modifier.fillMaxSize(),\n        contentAlignment = Alignment.Center\n    ) {\n        Column (modifier = Modifier.fillMaxSize(),\n            verticalArrangement = Arrangement.Center,\n            horizontalAlignment = Alignment.CenterHorizontally) {\n            Row (\n                modifier = Modifier.fillMaxSize().padding(top= 20.dp, bottom = 20.dp, end = 90.dp),\n                verticalAlignment = Alignment.CenterVertically,\n                horizontalArrangement = Arrangement.Center\n            ) {\n                Card(\n                    modifier = Modifier.padding(10.dp).weight(1.0f),\n                    shape = RoundedCornerShape(8.dp),\n                    elevation = 4.dp,\n                    onClick = {\n                        chooseImage(state) { file ->\n                            state.rawImage = getBufferedImage(file, state)\n                            state.currentImage = state.rawImage\n                            state.rawImageFile = file\n                        }\n                    },\n                    enabled = state.currentImage == null\n                ) {\n                    if (state.currentImage == null) {\n                        Text(\n                            modifier = Modifier.fillMaxSize().wrapContentSize(Alignment.Center),\n                            text = i18nState.getString(\"click_to_select_image\"),\n                            textAlign = TextAlign.Center\n                        )\n                    } else {\n                        Box {\n                            Column(\n                                modifier = Modifier.fillMaxSize(),\n                                verticalArrangement = Arrangement.Center,\n                                horizontalAlignment = Alignment.CenterHorizontally\n                            ) {\n                                Text(\n                                    text = \"source\",\n                                    textAlign = TextAlign.Center,\n                                    color = MaterialTheme.colors.primary,\n                                    fontSize = 36.sp,\n                                    fontWeight = FontWeight.Bold\n                                )\n\n                                Image(\n                                    painter = state.currentImage!!.toPainter(),\n                                    contentDescription = null,\n                                    contentScale = ContentScale.Fit,\n                                    modifier = Modifier\n                                )\n                            }\n\n                            Row(modifier = Modifier.align(Alignment.TopEnd)) {\n                                toolTipButton(text = \"上一步\",\n                                    buttonModifier = Modifier,\n                                    iconModifier = Modifier.size(20.dp),\n                                    painter = painterResource(\"images/doodle/previous_step.png\"),\n                                    onClick = {\n                                        viewModel.getLastSourceImage(state)\n                                    })\n\n                                toolTipButton(text = \"检测 source 图中的人脸\",\n                                    painter = painterResource(\"images/ai/face_landmark.png\"),\n                                    buttonModifier = Modifier,\n                                    iconModifier = Modifier.size(20.dp),\n                                    onClick = {\n                                        viewModel.faceLandMark(state, state.currentImage, state.rawImageFile, success = {\n                                            state.addQueue(state.currentImage!!)\n                                            state.currentImage = it\n                                        }, failure = {\n                                            showToast(\"算法服务异常\")\n                                        })\n                                    })\n\n                                toolTipButton(text = \"删除 source 的图\",\n                                    painter = painterResource(\"images/preview/delete.png\"),\n                                    buttonModifier = Modifier,\n                                    iconModifier = Modifier.size(20.dp),\n                                    onClick = {\n                                        state.clearImage()\n                                    })\n                            }\n                        }\n                    }\n                }\n\n                Card(\n                    modifier = Modifier.padding(10.dp).weight(1.0f),\n                    shape = RoundedCornerShape(8.dp),\n                    elevation = 4.dp,\n                    onClick = {\n                        chooseImage(state) { file ->\n                            viewModel.targetImage = getBufferedImage(file)\n                            viewModel.targetImageFile = file\n                        }\n                    },\n                    enabled = viewModel.targetImage == null\n                ) {\n                    if (viewModel.targetImage == null) {\n                        Text(\n                            modifier = Modifier.fillMaxSize().wrapContentSize(Alignment.Center),\n                            text = \"请点击选择图像\",\n                            textAlign = TextAlign.Center\n                        )\n                    } else {\n                        Box {\n                            Column(\n                                modifier = Modifier.fillMaxSize(),\n                                verticalArrangement = Arrangement.Center,\n                                horizontalAlignment = Alignment.CenterHorizontally\n                            ) {\n                                Text(\n                                    text = \"target\",\n                                    textAlign = TextAlign.Center,\n                                    color = MaterialTheme.colors.primary,\n                                    fontSize = 36.sp,\n                                    fontWeight = FontWeight.Bold\n                                )\n\n                                Image(\n                                    painter = viewModel.targetImage!!.toPainter(),\n                                    contentDescription = null,\n                                    contentScale = ContentScale.Fit,\n                                    modifier = Modifier)\n                            }\n\n                            Row(modifier = Modifier.align(Alignment.TopEnd)) {\n                                toolTipButton(text = \"上一步\",\n                                    buttonModifier = Modifier,\n                                    iconModifier = Modifier.size(20.dp),\n                                    painter = painterResource(\"images/doodle/previous_step.png\"),\n                                    onClick = {\n\n                                        if (viewModel.lastTargetImage!=null) {\n                                            viewModel.targetImage = viewModel.lastTargetImage\n                                        }\n                                    })\n\n                                toolTipButton(text = \"检测 target 图中的人脸\",\n                                    painter = painterResource(\"images/ai/face_landmark.png\"),\n                                    buttonModifier = Modifier,\n                                    iconModifier = Modifier.size(20.dp),\n                                    onClick = {\n                                        viewModel.faceLandMark(state, viewModel.targetImage, viewModel.targetImageFile, success = {\n                                            viewModel.lastTargetImage = viewModel.targetImage\n                                            viewModel.targetImage = it\n                                        }, failure = {\n                                            showToast(\"算法服务异常\")\n                                        })\n                                    })\n\n                                toolTipButton(text = \"删除 target 的图\",\n                                    painter = painterResource(\"images/preview/delete.png\"),\n                                    buttonModifier = Modifier,\n                                    iconModifier = Modifier.size(20.dp),\n                                    onClick = {\n                                        viewModel.clearTargetImage()\n                                    })\n                            }\n                        }\n                    }\n                }\n            }\n        }\n\n        rightSideMenuBar(modifier = Modifier.align(Alignment.CenterEnd)) {\n            toolTipButton(text = \"设置\",\n                painter = painterResource(\"images/cropimage/settings.png\"),\n                iconModifier = Modifier.size(36.dp),\n                onClick = {\n                    showSwapFaceSettings.value = true\n                })\n\n            toolTipButton(text = \"人脸替换\",\n                painter = painterResource(\"images/ai/face_swap2.png\"),\n                iconModifier = Modifier.size(36.dp),\n                onClick = {\n\n                    if (state.currentImage!=null && viewModel.targetImage!=null) {\n                        viewModel.faceSwap(state, state.currentImage, viewModel.targetImage, selectedOption.value, success = {\n                            viewModel.lastTargetImage = viewModel.targetImage\n                            viewModel.targetImage = it\n                        }, failure = {\n                            showToast(\"算法服务异常\")\n                        })\n                    }\n                })\n\n            toolTipButton(text = \"保存结果\",\n                painter = painterResource(\"images/doodle/save.png\"),\n                iconModifier = Modifier.size(36.dp),\n                onClick = {\n                    if (viewModel.targetImage!=null) {\n                        state.addQueue(state.currentImage!!)\n                        state.currentImage = viewModel.targetImage\n                    }\n                    state.togglePreviewWindow(false)\n                })\n        }\n\n        if (loadingDisplay) {\n            showLoading()\n        }\n\n        if (showToast) {\n            centerToast(message = toastMessage) {\n                showToast = false\n            }\n        }\n\n        if (showSwapFaceSettings.value) {\n            AlertDialog(onDismissRequest = {},\n                title = {\n                    Text(i18nState.getString(\"replace_target_face_count\"))\n                },\n                text = {\n                    Column {\n                        Row {\n                            RadioButton(\n                                selected = !selectedOption.value,\n                                onClick = { selectedOption.value = false }\n                            )\n                            Text(\"替换1个人脸\", modifier = Modifier.align(Alignment.CenterVertically))\n                        }\n\n                        Row {\n                            RadioButton(\n                                selected = selectedOption.value,\n                                onClick = { selectedOption.value = true }\n                            )\n                            Text(\"替换全部的人脸\", modifier = Modifier.align(Alignment.CenterVertically))\n                        }\n                    }\n                },\n                confirmButton = {\n                    Button(onClick = {\n                        showSwapFaceSettings.value = false\n                    }) {\n                        Text(i18nState.getString(\"close\"))\n                    }\n                })\n        }\n    }\n}\n\nprivate fun showToast(message: String) {\n    toastMessage = message\n    showToast = true\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/ai/faceswap/FaceSwapViewModel.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.ai.faceswap\n\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport cn.netdiscovery.monica.http.createRequest\nimport cn.netdiscovery.monica.http.createRequestBody\nimport cn.netdiscovery.monica.imageprocess.utils.writeImageFile\nimport cn.netdiscovery.monica.state.ApplicationState\nimport cn.netdiscovery.monica.utils.CVFailure\nimport cn.netdiscovery.monica.utils.CVSuccess\nimport cn.netdiscovery.monica.utils.ImageFormatDetector\nimport cn.netdiscovery.monica.utils.extensions.launchWithSuspendLoading\nimport cn.netdiscovery.monica.utils.logger\nimport okhttp3.*\nimport okhttp3.MediaType.Companion.toMediaType\nimport okhttp3.RequestBody.Companion.asRequestBody\nimport org.slf4j.Logger\nimport java.awt.image.BufferedImage\nimport java.io.File\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.ai.faceswap.FaceSwapModel\n * @author: Tony Shen\n * @date: 2024/8/25 14:55\n * @version: V1.0 <描述当前版本功能>\n */\nclass FaceSwapViewModel {\n    private val logger: Logger = logger<FaceSwapViewModel>()\n\n    var targetImage: BufferedImage? by mutableStateOf(null)\n    var targetImageFile: File? = null\n    var lastTargetImage: BufferedImage? by mutableStateOf(null)\n\n    fun clearTargetImage() {\n        if (targetImage!=null) {\n            targetImage = null\n        }\n\n        if (lastTargetImage!=null) {\n            lastTargetImage = null\n        }\n    }\n\n    fun faceLandMark(state: ApplicationState, image: BufferedImage?=null, file: File?=null,\n                     success:CVSuccess,\n                     failure:CVFailure) {\n\n        if (image == null || file == null) return\n\n        state.scope.launchWithSuspendLoading {\n            createRequest(request = {\n                val format = ImageFormatDetector.getImageFormat(file)?:\"jpg\"\n\n                val requestBody: RequestBody = createRequestBody(image ,format)\n\n                Request.Builder()\n                    .url( \"${state.algorithmUrlText}api/faceLandMark\")\n                    .post(requestBody)\n                    .build()\n            }, success = {\n                success.invoke(it)\n            }, failure = {\n                logger.error(it.message)\n                failure.invoke(it)\n            })\n        }\n    }\n\n    fun faceSwap(state: ApplicationState, image: BufferedImage?=null, target: BufferedImage?=null, status:Boolean,\n                 success:CVSuccess,\n                 failure:CVFailure) {\n\n        if (image == null || target == null) return\n\n        state.scope.launchWithSuspendLoading {\n            val srcFileName = \"temp_src.jpg\"\n            val targetFileName = \"temp_target.jpg\"\n            writeImageFile(image,srcFileName,\"jpg\")\n            writeImageFile(target,targetFileName,\"jpg\")\n\n            val srcFile = File(srcFileName)\n            val targetFile = File(targetFileName)\n\n            createRequest(request = {\n                // 构建 multipart 请求体\n                val requestBody = MultipartBody.Builder()\n                .setType(MultipartBody.FORM)\n                .addFormDataPart(\"src\", srcFileName, srcFile.asRequestBody(\"image/jpeg\".toMediaType()))\n                .addFormDataPart(\"target\", targetFileName, targetFile.asRequestBody(\"image/jpeg\".toMediaType()))\n                .build()\n\n                Request.Builder()\n                    .url(\"${state.algorithmUrlText}api/faceSwap?status=$status\")\n                    .post(requestBody)\n                    .build()\n            }, success = {\n                success.invoke(it)\n                srcFile.delete()\n                targetFile.delete()\n            }, failure = {\n                logger.error(it.message)\n                failure.invoke(it)\n            })\n        }\n    }\n\n    fun getLastSourceImage(state: ApplicationState) {\n        state.getLastImage()?.let {\n            state.currentImage = it\n        }\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/cartoon/CartoonView.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.cartoon\n\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.Card\nimport androidx.compose.material.ExperimentalMaterialApi\nimport androidx.compose.material.MaterialTheme\nimport androidx.compose.material.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.graphics.toPainter\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport cn.netdiscovery.monica.state.ApplicationState\nimport cn.netdiscovery.monica.ui.i18n.rememberI18nState\nimport cn.netdiscovery.monica.ui.widget.*\nimport cn.netdiscovery.monica.utils.chooseImage\nimport cn.netdiscovery.monica.utils.getBufferedImage\nimport loadingDisplay\nimport org.koin.compose.koinInject\nimport org.slf4j.Logger\nimport org.slf4j.LoggerFactory\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.cartoon.CartoonView\n * @author: Tony Shen\n * @date: 2025/4/16 17:32\n * @version: V1.0 <描述当前版本功能>\n */\nprivate val logger: Logger = LoggerFactory.getLogger(object : Any() {}.javaClass.enclosingClass)\n\nprivate var showToast by mutableStateOf(false)\nprivate var toastMessage by mutableStateOf(\"\")\n\n@OptIn(ExperimentalMaterialApi::class)\n@Composable\nfun cartoon(state: ApplicationState) {\n    val i18nState = rememberI18nState()\n    val viewModel: CartoonViewModel = koinInject()\n\n    Box(\n        modifier = Modifier\n            .fillMaxSize()\n            .background(\n                brush = Brush.verticalGradient(\n                    colors = listOf(\n                        MaterialTheme.colors.background,\n                        MaterialTheme.colors.surface\n                    )\n                )\n            ),\n        contentAlignment = Alignment.Center\n    ) {\n\n        Row (\n            modifier = Modifier.fillMaxSize().padding(bottom = 160.dp, end = 90.dp),\n            verticalAlignment = Alignment.CenterVertically,\n            horizontalArrangement = Arrangement.Center\n        ) {\n            Card(\n                modifier = Modifier.fillMaxSize().padding(10.dp),\n                shape = RoundedCornerShape(8.dp),\n                elevation = 4.dp,\n                onClick = {\n                    chooseImage(state) { file ->\n                        state.rawImage = getBufferedImage(file, state)\n                        state.currentImage = state.rawImage\n                        state.rawImageFile = file\n                    }\n                },\n                enabled = state.currentImage == null\n            ) {\n                if (state.currentImage == null) {\n                    Text(\n                        modifier = Modifier.fillMaxSize().wrapContentSize(Alignment.Center),\n                        text = i18nState.getString(\"click_to_select_image\"),\n                        textAlign = TextAlign.Center,\n                        fontSize = 24.sp\n                    )\n                } else {\n                    Image(\n                        painter = state.currentImage!!.toPainter(),\n                        contentDescription = null,\n                        contentScale = ContentScale.Fit,\n                        modifier = Modifier\n                    )\n                }\n            }\n        }\n\n        Column(modifier = Modifier.padding(start = 20.dp, bottom = 20.dp, top = 160.dp).align(Alignment.BottomStart)) {\n            subTitle(text = i18nState.getString(\"select_anime_style\"), modifier = Modifier.padding(start = 10.dp), fontWeight = FontWeight.Bold)\n\n            desktopLazyRow(modifier = Modifier.fillMaxWidth().padding(top = 10.dp).height(100.dp)) {\n                Card(\n                    elevation = 16.dp,\n                    modifier = Modifier.fillMaxSize().padding(start = 5.dp).clickable{\n\n                        viewModel.convert2Cartoon(state,1) {\n                            showToast(i18nState.getString(\"algorithm_service_error\"))\n                        }\n                    }\n                ) {\n                    Text(\n                        text = i18nState.getString(\"miyazaki_style\"), fontSize = 22.sp,\n                        color = MaterialTheme.colors.primary,\n                        modifier = Modifier.width(200.dp).wrapContentSize(Alignment.Center)\n                    )\n                }\n\n                Card(\n                    elevation = 16.dp,\n                    modifier = Modifier.fillMaxSize().padding(start = 5.dp).clickable{\n\n                        viewModel.convert2Cartoon(state,2) {\n                            showToast(i18nState.getString(\"algorithm_service_error\"))\n                        }\n                    }\n                ) {\n                    Text(\n                        text = i18nState.getString(\"japanese_portrait_style\"), fontSize = 22.sp,\n                        color = MaterialTheme.colors.primary,\n                        modifier = Modifier.width(200.dp).wrapContentSize(Alignment.Center)\n                    )\n                }\n\n                Card(\n                    elevation = 16.dp,\n                    modifier = Modifier.fillMaxSize().padding(start = 5.dp).clickable{\n\n                        viewModel.convert2Cartoon(state,3) {\n                            showToast(i18nState.getString(\"algorithm_service_error\"))\n                        }\n                    }\n                ) {\n                    Text(\n                        text = i18nState.getString(\"black_white_line_art\"), fontSize = 22.sp,\n                        color = MaterialTheme.colors.primary,\n                        modifier = Modifier.width(200.dp).wrapContentSize(Alignment.Center)\n                    )\n                }\n\n                Card(\n                    elevation = 16.dp,\n                    modifier = Modifier.fillMaxSize().padding(start = 5.dp).clickable{\n\n                        viewModel.convert2Cartoon(state,4) {\n                            showToast(i18nState.getString(\"algorithm_service_error\"))\n                        }\n                    }\n                ) {\n                    Text(\n                        text = i18nState.getString(\"shinkai_style\"), fontSize = 22.sp,\n                        color = MaterialTheme.colors.primary,\n                        modifier = Modifier.width(200.dp).wrapContentSize(Alignment.Center)\n                    )\n                }\n\n                Card(\n                    elevation = 16.dp,\n                    modifier = Modifier.fillMaxSize().padding(start = 5.dp).clickable{\n\n                        viewModel.convert2Cartoon(state,5) {\n                            showToast(i18nState.getString(\"algorithm_service_error\"))\n                        }\n                    }\n                ) {\n                    Text(\n                        text = i18nState.getString(\"cute_style\"), fontSize = 22.sp,\n                        color = MaterialTheme.colors.primary,\n                        modifier = Modifier.width(200.dp).wrapContentSize(Alignment.Center)\n                    )\n                }\n            }\n        }\n\n        rightSideMenuBar(modifier = Modifier.align(Alignment.CenterEnd)) {\n\n            toolTipButton(text = i18nState.getString(\"delete\"),\n                painter = painterResource(\"images/preview/delete.png\"),\n                iconModifier = Modifier.size(36.dp),\n                onClick = {\n                    state.clearImage()\n                })\n\n            toolTipButton(text = i18nState.getString(\"previous_step\"),\n                painter = painterResource(\"images/doodle/previous_step.png\"),\n                onClick = {\n                    state.getLastImage()?.let {\n                        state.currentImage = it\n                    }\n                })\n\n            toolTipButton(text = i18nState.getString(\"save\"),\n                painter = painterResource(\"images/doodle/save.png\"),\n                onClick = {\n                    state.closePreviewWindow()\n                })\n        }\n\n        if (loadingDisplay) {\n            showLoading()\n        }\n\n        if (showToast) {\n            centerToast(message = toastMessage) {\n                showToast = false\n            }\n        }\n    }\n}\n\nprivate fun showToast(message: String) {\n    toastMessage = message\n    showToast = true\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/cartoon/CartoonViewModel.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.cartoon\n\nimport cn.netdiscovery.monica.http.createRequest\nimport cn.netdiscovery.monica.http.createRequestBody\nimport cn.netdiscovery.monica.state.ApplicationState\nimport cn.netdiscovery.monica.utils.CVFailure\nimport cn.netdiscovery.monica.utils.ImageFormatDetector\nimport cn.netdiscovery.monica.utils.extensions.launchWithSuspendLoading\nimport cn.netdiscovery.monica.utils.logger\nimport okhttp3.Request\nimport okhttp3.RequestBody\nimport org.slf4j.Logger\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.cartoon.CartoonViewModel\n * @author: Tony Shen\n * @date: 2025/4/16 18:21\n * @version: V1.0 <描述当前版本功能>\n */\nclass CartoonViewModel {\n\n    private val logger: Logger = logger<CartoonViewModel>()\n\n    fun convert2Cartoon(state: ApplicationState, type:Int, failure: CVFailure) {\n        if (state.currentImage == null) return\n\n        state.scope.launchWithSuspendLoading {\n\n            createRequest(request = {\n                val format = ImageFormatDetector.getImageFormat(state.rawImageFile!!)?:\"jpg\"\n\n                val requestBody: RequestBody = createRequestBody(state.currentImage!!,format)\n\n                Request.Builder()\n                    .url( \"${state.algorithmUrlText}api/cartoon?type=$type\")\n                    .post(requestBody)\n                    .build()\n            }, success = {\n                state.addQueue(state.currentImage!!)\n                state.currentImage = it\n            }, failure = {\n                logger.error(it.message)\n                failure.invoke(it)\n            })\n        }\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/colorcorrection/ColorCorrectionView.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.colorcorrection\n\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.Card\nimport androidx.compose.material.ExperimentalMaterialApi\nimport androidx.compose.material.MaterialTheme\nimport androidx.compose.material.Slider\nimport androidx.compose.material.SliderDefaults\nimport androidx.compose.material.Text\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.graphics.toComposeImageBitmap\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.unit.dp\nimport cn.netdiscovery.monica.config.MODULE_COLOR\nimport cn.netdiscovery.monica.domain.ColorCorrectionSettings\nimport cn.netdiscovery.monica.history.EditHistoryCenter\nimport cn.netdiscovery.monica.history.modules.colorcorrection.ColorCorrectionParams\nimport cn.netdiscovery.monica.history.modules.colorcorrection.recordColorCorrection\nimport cn.netdiscovery.monica.llm.DialogSession\nimport cn.netdiscovery.monica.llm.systemPromptForColorCorrection\nimport cn.netdiscovery.monica.state.ApplicationState\nimport cn.netdiscovery.monica.ui.widget.PageLifecycle\nimport cn.netdiscovery.monica.ui.widget.showLoading\nimport cn.netdiscovery.monica.ui.widget.toolTipButton\nimport cn.netdiscovery.monica.ui.widget.image.ImageSizeCalculator\nimport cn.netdiscovery.monica.utils.extensions.to2fStr\nimport cn.netdiscovery.monica.i18n.getCurrentStringResource\nimport cn.netdiscovery.monica.imageprocess.utils.extension.image2ByteArray\nimport cn.netdiscovery.monica.manager.OpenCVManager\nimport cn.netdiscovery.monica.opencv.ImageProcess\nimport cn.netdiscovery.monica.utils.extensions.launchWithSuspendLoading\nimport com.safframework.rxcache.utils.GsonUtils\nimport loadingDisplay\nimport org.koin.compose.koinInject\nimport org.slf4j.Logger\nimport org.slf4j.LoggerFactory\nimport kotlin.math.roundToInt\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.colorcorrection.ColorCorrectionView\n * @author: Tony Shen\n * @date: 2024/11/5 15:05\n * @version: V1.0 <描述当前版本功能>\n */\nprivate val logger: Logger = LoggerFactory.getLogger(object : Any() {}.javaClass.enclosingClass)\n\nvar colorCorrectionSettings = ColorCorrectionSettings()\nprivate var showLLMDialog by mutableStateOf(false)\n\n@OptIn(ExperimentalMaterialApi::class)\n@Composable\nfun colorCorrection(state: ApplicationState) {\n    val viewModel: ColorCorrectionViewModel = koinInject()\n    val density = LocalDensity.current\n    val i18nState = getCurrentStringResource()\n\n    var cachedImage by remember { mutableStateOf(state.currentImage!!) } // 缓存 state.currentImage\n\n    val enableSlider = !loadingDisplay\n\n    val session = remember { DialogSession(systemPromptForColorCorrection, colorCorrectionSettings) }\n    \n    // 使用统一的图片尺寸计算\n    val (imageWidth, imageHeight) = ImageSizeCalculator.calculateImageSize(state)\n    \n    // 获取原始图片尺寸和显示尺寸，用于坐标转换\n    val originalSize = ImageSizeCalculator.getImagePixelSize(state)\n\n    PageLifecycle(\n        onInit = {\n            logger.info(\"ColorCorrectionView 启动时初始化\")\n        },\n        onDisposeEffect = {\n            logger.info(\"ColorCorrectionView 关闭时释放资源\")\n            viewModel.clearAllStatus()\n        }\n    )\n\n    Box(\n        modifier = Modifier\n            .fillMaxSize()\n            .background(\n                brush = Brush.verticalGradient(\n                    colors = listOf(\n                        MaterialTheme.colors.background,\n                        MaterialTheme.colors.surface\n                    )\n                )\n            ),\n        contentAlignment = Alignment.Center\n    ) {\n        Row (\n            modifier = Modifier.fillMaxSize(),\n            verticalAlignment = Alignment.CenterVertically,\n            horizontalArrangement = Arrangement.Center\n        ) {\n            Card(\n                modifier = Modifier\n                    .padding(10.dp)\n                    .weight(1.4f)\n                    .width(imageWidth)\n                    .height(imageHeight),\n                shape = RoundedCornerShape(8.dp),\n                elevation = 4.dp,\n                onClick = {\n                },\n                enabled = false\n            ) {\n                Image(\n                    bitmap = cachedImage.toComposeImageBitmap(),\n                    contentDescription = null,\n                    contentScale = ContentScale.Fit,\n                    modifier = Modifier.fillMaxSize()\n                )\n            }\n\n            Row(modifier = Modifier.weight(0.6f)\n                .padding(start = 10.dp, end = 10.dp)\n                .background(color = MaterialTheme.colors.surface, shape = RoundedCornerShape(5))) {\n                Column(\n                    Modifier.padding(20.dp),\n                    verticalArrangement = Arrangement.Center\n                ) {\n                    Row(verticalAlignment = Alignment.CenterVertically) {\n                        Text(modifier = Modifier.width(100.dp), text = i18nState.get(\"contrast\") + \"：\", color = MaterialTheme.colors.onSurface)\n\n                        Row(verticalAlignment = Alignment.CenterVertically) {\n                            Slider(\n                                value = viewModel.contrast,\n                                onValueChange = {\n                                    val value = it.roundToInt()\n                                    viewModel.contrast = value.toFloat()\n                                    colorCorrectionSettings = colorCorrectionSettings.copy(contrast = value, status = 1)\n\n                                    viewModel.colorCorrection(state, cachedImage, colorCorrectionSettings)  { image-> cachedImage = image }\n                                },\n                                enabled = enableSlider,\n                                modifier = Modifier.weight(9f),\n                                valueRange = 0f..510f,\n                                colors = SliderDefaults.colors())\n\n                            Text(\n                                text = viewModel.contrast.to2fStr(),\n                                color = MaterialTheme.colors.onSurface,\n                                modifier = Modifier.weight(1f))\n                        }\n                    }\n\n                    Row(verticalAlignment = Alignment.CenterVertically) {\n                        Text(modifier = Modifier.width(100.dp), text = i18nState.get(\"hue\") + \"：\", color = MaterialTheme.colors.onSurface)\n\n                        Row(verticalAlignment = Alignment.CenterVertically) {\n                            Slider(\n                                value = viewModel.hue,\n                                onValueChange = {\n                                    val value = it.roundToInt()\n                                    viewModel.hue = value.toFloat()\n                                    colorCorrectionSettings = colorCorrectionSettings.copy(hue = value, status = 2)\n\n                                    viewModel.colorCorrection(state, cachedImage, colorCorrectionSettings)  { image-> cachedImage = image }\n                                },\n                                enabled = enableSlider,\n                                modifier = Modifier.weight(9f),\n                                valueRange = 0f..360f)\n\n                            Text(\n                                text = viewModel.hue.to2fStr(),\n                                color = MaterialTheme.colors.onSurface,\n                                modifier = Modifier.weight(1f))\n                        }\n                    }\n\n                    Row(verticalAlignment = Alignment.CenterVertically) {\n                        Text(modifier = Modifier.width(100.dp), text = i18nState.get(\"saturation\") + \"：\", color = MaterialTheme.colors.onSurface)\n\n                        Row(verticalAlignment = Alignment.CenterVertically) {\n                            Slider(\n                                value = viewModel.saturation,\n                                onValueChange = {\n                                    val value = it.roundToInt()\n                                    viewModel.saturation = value.toFloat()\n\n                                    colorCorrectionSettings = colorCorrectionSettings.copy(saturation = value, status = 3)\n\n                                    viewModel.colorCorrection(state, cachedImage, colorCorrectionSettings)  { image-> cachedImage = image }\n                                },\n                                enabled = enableSlider,\n                                modifier = Modifier.weight(9f),\n                                valueRange = 0f..510f,\n                                colors = SliderDefaults.colors())\n\n                            Text(\n                                text = viewModel.saturation.to2fStr(),\n                                color = MaterialTheme.colors.onSurface,\n                                modifier = Modifier.weight(1f))\n                        }\n                    }\n\n                    Row(verticalAlignment = Alignment.CenterVertically) {\n                        Text(modifier = Modifier.width(100.dp), text = i18nState.get(\"lightness\") + \"：\", color = MaterialTheme.colors.onSurface)\n\n                        Row(verticalAlignment = Alignment.CenterVertically) {\n                            Slider(\n                                value = viewModel.lightness,\n                                onValueChange = {\n                                    val value = it.roundToInt()\n                                    viewModel.lightness = value.toFloat()\n\n                                    colorCorrectionSettings = colorCorrectionSettings.copy(lightness = value, status = 4)\n\n                                    viewModel.colorCorrection(state, cachedImage, colorCorrectionSettings)  { image-> cachedImage = image }\n                                },\n                                enabled = enableSlider,\n                                modifier = Modifier.weight(9f),\n                                valueRange = 0f..510f,\n                                colors = SliderDefaults.colors())\n\n                            Text(\n                                text = viewModel.lightness.to2fStr(),\n                                color = MaterialTheme.colors.onSurface,\n                                modifier = Modifier.weight(1f))\n                        }\n                    }\n\n                    Row(verticalAlignment = Alignment.CenterVertically) {\n                        Text(modifier = Modifier.width(100.dp), text = i18nState.get(\"temperature\") + \"：\", color = MaterialTheme.colors.onSurface)\n\n                        Row(verticalAlignment = Alignment.CenterVertically) {\n                            Slider(\n                                value = viewModel.temperature,\n                                onValueChange = {\n                                    val value = it.roundToInt()\n                                    viewModel.temperature = value.toFloat()\n\n                                    colorCorrectionSettings = colorCorrectionSettings.copy(temperature = value, status = 5)\n\n                                    viewModel.colorCorrection(state, cachedImage, colorCorrectionSettings)  { image-> cachedImage = image }\n                                },\n                                enabled = enableSlider,\n                                modifier = Modifier.weight(9f),\n                                valueRange = 0f..510f,\n                                colors = SliderDefaults.colors())\n\n                            Text(\n                                text = viewModel.temperature.to2fStr(),\n                                color = MaterialTheme.colors.onSurface,\n                                modifier = Modifier.weight(1f))\n                        }\n                    }\n\n                    Row(verticalAlignment = Alignment.CenterVertically) {\n                        Text(modifier = Modifier.width(100.dp), text = i18nState.get(\"highlight\") + \"：\", color = MaterialTheme.colors.onSurface)\n\n                        Row(verticalAlignment = Alignment.CenterVertically) {\n                            Slider(\n                                value = viewModel.highlight,\n                                onValueChange = {\n                                    val value = it.roundToInt()\n                                    viewModel.highlight = value.toFloat()\n\n                                    colorCorrectionSettings = colorCorrectionSettings.copy(highlight = value, status = 6)\n\n                                    viewModel.colorCorrection(state, cachedImage, colorCorrectionSettings)  { image-> cachedImage = image }\n                                },\n                                enabled = enableSlider,\n                                modifier = Modifier.weight(9f),\n                                valueRange = 0f..510f,\n                                colors = SliderDefaults.colors())\n\n                            Text(\n                                text = viewModel.highlight.to2fStr(),\n                                color = MaterialTheme.colors.onSurface,\n                                modifier = Modifier.weight(1f))\n                        }\n                    }\n\n                    Row(verticalAlignment = Alignment.CenterVertically) {\n                        Text(modifier = Modifier.width(100.dp), text = i18nState.get(\"shadow\") + \"：\", color = MaterialTheme.colors.onSurface)\n\n                        Row(verticalAlignment = Alignment.CenterVertically) {\n                            Slider(\n                                value = viewModel.shadow,\n                                onValueChange = {\n                                    val value = it.roundToInt()\n                                    viewModel.shadow = value.toFloat()\n\n                                    colorCorrectionSettings = colorCorrectionSettings.copy(shadow = value, status = 7)\n\n                                    viewModel.colorCorrection(state, cachedImage, colorCorrectionSettings)  { image-> cachedImage = image }\n                                },\n                                enabled = enableSlider,\n                                modifier = Modifier.weight(9f),\n                                valueRange = 0f..510f,\n                                colors = SliderDefaults.colors())\n\n                            Text(\n                                text = viewModel.shadow.to2fStr(),\n                                color = MaterialTheme.colors.onSurface,\n                                modifier = Modifier.weight(1f))\n                        }\n                    }\n\n                    Row(verticalAlignment = Alignment.CenterVertically) {\n                        Text(modifier = Modifier.width(100.dp), text = i18nState.get(\"sharpen\") + \"：\", color = MaterialTheme.colors.onSurface)\n\n                        Row(verticalAlignment = Alignment.CenterVertically) {\n                            Slider(\n                                value = viewModel.sharpen,\n                                onValueChange = {\n                                    val value = it.roundToInt()\n                                    viewModel.sharpen = value.toFloat()\n\n                                    colorCorrectionSettings = colorCorrectionSettings.copy(sharpen = value, status = 8)\n\n                                    viewModel.colorCorrection(state, cachedImage, colorCorrectionSettings)  { image-> cachedImage = image }\n                                },\n                                enabled = enableSlider,\n                                modifier = Modifier.weight(9f),\n                                valueRange = 0f..255f)\n\n                            Text(\n                                text = viewModel.sharpen.to2fStr(),\n                                color = MaterialTheme.colors.onSurface,\n                                modifier = Modifier.weight(1f))\n                        }\n                    }\n\n                    Row(verticalAlignment = Alignment.CenterVertically) {\n                        Text(modifier = Modifier.width(100.dp), text = i18nState.get(\"corner\") + \"：\", color = MaterialTheme.colors.onSurface)\n\n                        Row(verticalAlignment = Alignment.CenterVertically) {\n                            Slider(\n                                value = viewModel.corner,\n                                onValueChange = {\n                                    val value = it.roundToInt()\n                                    viewModel.corner = value.toFloat()\n\n                                    colorCorrectionSettings = colorCorrectionSettings.copy(corner = value, status = 9)\n\n                                    viewModel.colorCorrection(state, cachedImage, colorCorrectionSettings)  { image-> cachedImage = image }\n                                },\n                                enabled = enableSlider,\n                                modifier = Modifier.weight(9f),\n                                valueRange = 0f..255f)\n\n                            Text(\n                                text = viewModel.corner.to2fStr(),\n                                color = MaterialTheme.colors.onSurface,\n                                modifier = Modifier.weight(1f))\n                        }\n                    }\n\n                    // 底部菜单\n                    Row(modifier = Modifier.align(Alignment.CenterHorizontally).padding(top = 10.dp)) {\n                        // 保存\n                        toolTipButton(text = i18nState.get(\"save\"),\n                            painter = painterResource(\"images/doodle/save.png\"),\n                            iconModifier = Modifier.size(36.dp),\n                            onClick = {\n                                viewModel.save(state) {\n                                    state.addQueue(state.currentImage!!)\n                                    state.currentImage = cachedImage\n                                    state.togglePreviewWindow(false)\n                                }\n                            })\n\n                        // 自然语言图像调色\n                        toolTipButton(text = i18nState.get(\"natural_language_color_correction\"),\n                            painter = painterResource(\"images/colorcorrection/chatbot.png\"),\n                            iconModifier = Modifier.size(36.dp),\n                            onClick = {\n                                showLLMDialog = true\n                            })\n\n                        // 上一步\n                        toolTipButton(text = i18nState.get(\"previous_step\"),\n                            painter = painterResource(\"images/doodle/previous_step.png\"),\n                            iconModifier = Modifier.size(36.dp),\n                            onClick = {\n                                viewModel.undo { lastSettings->\n                                    logger.info(\"lastSettings = ${lastSettings}\")\n                                    val lastStatus = colorCorrectionSettings.status\n                                    colorCorrectionSettings = lastSettings.copy(status = lastStatus)\n                                    viewModel.colorCorrection(state, cachedImage, colorCorrectionSettings,false)  { image-> cachedImage = image }\n                                }\n                            })\n\n                        // 撤回\n                        toolTipButton(text = i18nState.get(\"revoke\"),\n                            painter = painterResource(\"images/doodle/revoke.png\"),\n                            onClick = {\n                                viewModel.redo { lastSettings->\n                                    val lastStatus = colorCorrectionSettings.status\n                                    colorCorrectionSettings = lastSettings.copy(status = lastStatus)\n                                    viewModel.colorCorrection(state, cachedImage, colorCorrectionSettings, false)  { image-> cachedImage = image }\n                                }\n                            })\n                    }\n                }\n            }\n        }\n\n        if (loadingDisplay) {\n            showLoading()\n        }\n\n        if (showLLMDialog) {\n            NaturalLanguageDialog(showLLMDialog, session, state.deepSeekApiKeyText, state.geminiApiKeyText, onDismissRequest = {\n                showLLMDialog = false\n            }) {\n                colorCorrectionSettings = it\n                viewModel.updateParams(colorCorrectionSettings)\n                viewModel.colorCorrection(state, cachedImage, colorCorrectionSettings)  { image -> cachedImage = image }\n            }\n        }\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/colorcorrection/ColorCorrectionViewModel.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.colorcorrection\n\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport cn.netdiscovery.monica.config.MODULE_COLOR\nimport cn.netdiscovery.monica.domain.ColorCorrectionSettings\nimport cn.netdiscovery.monica.history.EditHistoryCenter\nimport cn.netdiscovery.monica.history.modules.colorcorrection.ColorCorrectionParams\nimport cn.netdiscovery.monica.history.modules.colorcorrection.recordColorCorrection\nimport cn.netdiscovery.monica.imageprocess.BufferedImages\nimport cn.netdiscovery.monica.imageprocess.utils.extension.image2ByteArray\nimport cn.netdiscovery.monica.manager.OpenCVManager\nimport cn.netdiscovery.monica.opencv.ImageProcess\nimport cn.netdiscovery.monica.state.ApplicationState\nimport cn.netdiscovery.monica.utils.*\nimport cn.netdiscovery.monica.utils.extensions.launchWithLoading\nimport cn.netdiscovery.monica.utils.extensions.launchWithSuspendLoading\nimport com.safframework.rxcache.utils.GsonUtils\nimport org.slf4j.Logger\nimport java.awt.image.BufferedImage\nimport java.util.concurrent.atomic.AtomicBoolean\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.colorcorrection.ColorCorrectionViewModel\n * @author: Tony Shen\n * @date: 2024/11/5 15:17\n * @version: V1.0 <描述当前版本功能>\n */\nclass ColorCorrectionViewModel {\n\n    private val logger: Logger = logger<ColorCorrectionViewModel>()\n    private val manager = EditHistoryCenter.getManager<ColorCorrectionParams>(MODULE_COLOR)\n\n    var contrast by mutableStateOf(255f )\n    var hue by mutableStateOf(180f )\n    var saturation by mutableStateOf(255f )\n    var lightness by mutableStateOf(255f )\n    var temperature by mutableStateOf(255f )\n    var highlight by mutableStateOf(255f )\n    var shadow by mutableStateOf(255f )\n    var sharpen by mutableStateOf(0f )\n    var corner by mutableStateOf(0f )\n\n    private var cppObjectPtr:Long = 0\n\n    private var init:AtomicBoolean = AtomicBoolean(false)\n\n    fun updateParams(params: ColorCorrectionSettings) {\n        contrast = params.contrast.toFloat()\n        hue = params.hue.toFloat()\n        saturation = params.saturation.toFloat()\n        lightness = params.lightness.toFloat()\n        temperature = params.temperature.toFloat()\n        highlight = params.highlight.toFloat()\n        shadow = params.shadow.toFloat()\n        sharpen = params.sharpen.toFloat()\n        corner = params.corner.toFloat()\n    }\n\n    /**\n     * 封装图像调色的方法\n     * @param state   当前应用的 state\n     * @param image   需要调色的图像\n     * @param colorCorrectionSettings 图像调色所需要的参数\n     * @param isPush  是否需要记录一次新的编辑操作。默认为 true，如果使用了 undo()、redo() 操作就为 false\n     * @param success 成功后的回调\n     */\n    fun colorCorrection(state: ApplicationState,\n                        image: BufferedImage,\n                        colorCorrectionSettings: ColorCorrectionSettings,\n                        isPush: Boolean = true,\n                        success: CVSuccess) {\n\n        logger.info(\"colorCorrectionSettings = ${GsonUtils.toJson(colorCorrectionSettings)}\")\n\n        state.scope.launchWithSuspendLoading {\n            if (!init.get()) {\n                init.set(true)\n\n                val byteArray = image.image2ByteArray()\n                cppObjectPtr = ImageProcess.initColorCorrection(byteArray)\n            }\n\n            OpenCVManager.invokeCV(image,\n                action  = { byteArray ->\n                    manager.recordColorCorrection(operation = \"colorCorrection\", isPush = isPush, colorCorrectionSettings = colorCorrectionSettings)\n\n                    ImageProcess.colorCorrection(byteArray, colorCorrectionSettings, cppObjectPtr)\n                },\n                success = { success.invoke(it) },\n                failure = { e ->\n                    logger.error(\"colorCorrection is failed\", e)\n                })\n        }\n    }\n\n    /**\n     * 保存图像\n     */\n    fun save(state: ApplicationState, action: Action) {\n\n        val imageFormat = state.rawImageFormat\n\n        if (imageFormat!=null && imageFormat.isRaw()) {\n            state.scope.launchWithLoading {\n\n                if (!state.nativeFullImageProcessed) {\n                    val filePath = state.rawImageFile?.absolutePath!!\n                    val nativePtr = state.nativeImageInfo?.nativePtr!!\n\n                    // 获取全尺寸的 raw 图像，更新金字塔对象，完成调色返回预览对象\n                    val previewImage = ImageProcess.decodeRawAndColorCorrection(filePath, nativePtr, colorCorrectionSettings, cppObjectPtr)\n                    if (previewImage!=null) {\n                        state.addQueue(state.currentImage!!)\n                        val image = BufferedImages.toBufferedImage(previewImage.previewImage, previewImage.width, previewImage.height, BufferedImage.TYPE_INT_ARGB)\n                        state.currentImage = image\n                        state.nativeFullImageProcessed = true\n                        state.togglePreviewWindow(false)\n                    }\n                } else {\n                    val nativePtr = state.nativeImageInfo?.nativePtr!!\n\n                    // 更新金字塔对象，完成调色返回预览对象\n                    val previewImage = ImageProcess.colorCorrectionWithPyramidImage(nativePtr, colorCorrectionSettings, cppObjectPtr)\n                    if (previewImage!=null) {\n                        state.addQueue(state.currentImage!!)\n                        val image = BufferedImages.toBufferedImage(previewImage.previewImage, previewImage.width, previewImage.height, BufferedImage.TYPE_INT_ARGB)\n                        state.currentImage = image\n                        state.togglePreviewWindow(false)\n                    }\n                }\n            }\n        } else if (imageFormat!=null && imageFormat == ImageFormat.HEIC) {\n            state.scope.launchWithLoading {\n\n                val nativePtr = state.nativeImageInfo?.nativePtr!!\n\n                // 更新金字塔对象，完成调色返回预览对象\n                val previewImage = ImageProcess.colorCorrectionWithPyramidImage(nativePtr, colorCorrectionSettings, cppObjectPtr)\n                if (previewImage!=null) {\n                    state.addQueue(state.currentImage!!)\n                    val image = BufferedImages.toBufferedImage(previewImage.previewImage, previewImage.width, previewImage.height, BufferedImage.TYPE_INT_ARGB)\n                    state.currentImage = image\n                    state.togglePreviewWindow(false)\n                }\n            }\n        } else {\n            action.invoke()\n        }\n    }\n\n    fun undo(block: (ColorCorrectionSettings)-> Unit) {\n        val pair = manager.undo()\n\n        if (pair!=null) {\n            val lastSettings = pair.first.toSettings()\n            updateParams(lastSettings)\n            block.invoke(lastSettings)\n        }\n    }\n\n    fun redo(block: (ColorCorrectionSettings)-> Unit) {\n        val pair = manager.redo()\n\n        if (pair!=null) {\n            val lastSettings = pair.first.toSettings()\n            updateParams(lastSettings)\n            block.invoke(lastSettings)\n        }\n    }\n\n    fun clearAllStatus() {\n        init.set(false)\n\n        contrast = 255f\n        hue = 180f\n        saturation = 255f\n        lightness = 255f\n        temperature = 255f\n        highlight = 255f\n        shadow = 255f\n        sharpen = 0f\n        corner = 0f\n\n        colorCorrectionSettings = ColorCorrectionSettings()\n\n        if (cppObjectPtr !=0L ) {\n            ImageProcess.deleteColorCorrection(cppObjectPtr)\n            cppObjectPtr = 0\n        }\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/colorcorrection/NaturalLanguageDialog.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.colorcorrection\n\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.runtime.*\nimport androidx.compose.material.AlertDialog\nimport androidx.compose.material.Text\nimport androidx.compose.material.TextButton\nimport androidx.compose.material.OutlinedTextField\nimport androidx.compose.material.CircularProgressIndicator\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport cn.netdiscovery.monica.domain.ColorCorrectionSettings\nimport cn.netdiscovery.monica.llm.DialogSession\nimport cn.netdiscovery.monica.i18n.getCurrentStringResource\nimport cn.netdiscovery.monica.llm.LLMProvider\nimport cn.netdiscovery.monica.llm.rememberLLMServiceManager\nimport cn.netdiscovery.monica.ui.widget.divider\nimport kotlinx.coroutines.*\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.colorcorrection.NaturalLanguageDialog\n * @author: Tony Shen\n * @date: 2025/8/4 14:00\n * @version: V1.0 <描述当前版本功能>\n */\n@Composable\nfun NaturalLanguageDialog(\n    visible: Boolean,\n    session: DialogSession,\n    deepSeekApiKey: String,\n    geminiApiKey: String,\n    onDismissRequest: () -> Unit,\n    onConfirm: (ColorCorrectionSettings) -> Unit\n) {\n    val i18nState = getCurrentStringResource()\n    val llmServiceManager = rememberLLMServiceManager()\n    var inputText by remember { mutableStateOf(\"\") }\n    var loading by remember { mutableStateOf(false) }\n    var errorMessage by remember { mutableStateOf<String?>(null) }\n    \n    // 记住用户上次选择的 LLM 提供商\n    var selectedProvider by remember { \n        mutableStateOf(session.lastUsedProvider ?: LLMProvider.DEEPSEEK) \n    }\n    \n    // 当对话框打开时，如果有历史记录，尝试推断上次使用的提供商\n    LaunchedEffect(visible) {\n        if (visible && session.history.isNotEmpty()) {\n            // 从历史记录中推断上次使用的提供商\n            // 这里可以根据历史记录的特征来判断，暂时保持默认选择\n            // 未来可以考虑在 DialogSession 中添加 provider 字段来记录\n        }\n    }\n    \n    // 检查当前选择的提供商是否有 API Key\n    val hasApiKey = when (selectedProvider) {\n        LLMProvider.DEEPSEEK -> deepSeekApiKey.isNotBlank()\n        LLMProvider.GEMINI -> geminiApiKey.isNotBlank()\n    }\n\n    if (visible) {\n        AlertDialog(\n            onDismissRequest = onDismissRequest,\n            title = { Text(i18nState.get(\"natural_language_color_correction\")) },\n            text = {\n                Column(modifier = Modifier.fillMaxWidth().heightIn(min = 200.dp, max = 500.dp)) {\n\n                    // AI 服务提供商选择\n                    Row(\n                        modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp),\n                        verticalAlignment = Alignment.CenterVertically\n                    ) {\n                        Text(\n                            text = i18nState.get(\"ai_provider_selection\") + \": \",\n                            fontWeight = FontWeight.Medium\n                        )\n                        \n                        androidx.compose.material.RadioButton(\n                            selected = selectedProvider == LLMProvider.DEEPSEEK,\n                            onClick = { selectedProvider = LLMProvider.DEEPSEEK }\n                        )\n                        Text(\n                            text = i18nState.get(\"ai_provider_deepseek\"),\n                            modifier = Modifier.padding(end = 16.dp)\n                        )\n                        \n                        androidx.compose.material.RadioButton(\n                            selected = selectedProvider == LLMProvider.GEMINI,\n                            onClick = { selectedProvider = LLMProvider.GEMINI }\n                        )\n                        Text(text = i18nState.get(\"ai_provider_gemini\"))\n                    }\n                    \n                    // API Key 状态提示\n                    if (!hasApiKey) {\n                        Row(\n                            modifier = Modifier.fillMaxWidth().padding(top = 8.dp),\n                            verticalAlignment = Alignment.CenterVertically\n                        ) {\n                            Text(\n                                text = \"⚠️ \",\n                                fontSize = 16.sp,\n                                color = Color(0xFFFF8C00), // Orange\n                                modifier = Modifier.padding(end = 4.dp)\n                            )\n                            Text(\n                                text = i18nState.get(\"api_key_required\"),\n                                fontSize = 12.sp,\n                                color = Color(0xFFFF8C00) // Orange\n                            )\n                        }\n                    }\n\n                    // 上下文对话记录区\n                    if (session.history.isNotEmpty()) {\n                        LazyColumn(\n                            modifier = Modifier\n                                .fillMaxWidth()\n                                .weight(1f)\n                                .padding(4.dp)\n                        ) {\n                            items(session.history) { historyItem ->\n                                Column(modifier = Modifier.padding(vertical = 4.dp)) {\n                                    Row(\n                                        modifier = Modifier.fillMaxWidth(),\n                                        verticalAlignment = Alignment.CenterVertically\n                                    ) {\n                                        Text(\"👤 ${historyItem.userInstruction}\", fontWeight = FontWeight.Bold)\n                                        Spacer(modifier = Modifier.weight(1f))\n                                        // 显示使用的 LLM 提供商\n                                        Text(\n                                            text = when (historyItem.usedProvider) {\n                                                LLMProvider.DEEPSEEK -> \"🤖 ${i18nState.get(\"ai_provider_deepseek\")}\"\n                                                LLMProvider.GEMINI -> \"🤖 ${i18nState.get(\"ai_provider_gemini\")}\"\n                                            },\n                                            fontSize = 11.sp,\n                                            color = Color.Gray\n                                        )\n                                    }\n                                    Text(\n                                        i18nState.get(\"update_parameters\") + formatSettingsDiff(historyItem.resultSettings, i18nState), \n                                        fontSize = 13.sp\n                                    )\n                                }\n                            }\n                        }\n                        divider()\n                    }\n\n                    // 输入框\n                    OutlinedTextField(\n                        value = inputText,\n                        onValueChange = { inputText = it },\n                        label = { Text(i18nState.get(\"enter_color_instruction\")) },\n                        singleLine = false,\n                        maxLines = 4,\n                        modifier = Modifier.fillMaxWidth()\n                    )\n\n                    // 进度与错误提示\n                    if (loading) {\n                        Spacer(modifier = Modifier.height(10.dp))\n                        CircularProgressIndicator(modifier = Modifier.size(24.dp))\n                    }\n                    errorMessage?.let {\n                        Spacer(modifier = Modifier.height(10.dp))\n                        Text(it, color = Color.Red)\n                    }\n                }\n            },\n            confirmButton = {\n                TextButton(\n                    onClick = {\n                        loading = true\n                        errorMessage = null\n                        CoroutineScope(Dispatchers.IO).launch {\n                            try {\n                                val apiKey = when (selectedProvider) {\n                                    LLMProvider.DEEPSEEK -> deepSeekApiKey\n                                    LLMProvider.GEMINI -> geminiApiKey\n                                }\n                                \n                                // 检查 API Key 是否已配置\n                                if (apiKey.isBlank()) {\n                                    withContext(Dispatchers.Main) {\n                                        errorMessage = when (selectedProvider) {\n                                            LLMProvider.DEEPSEEK -> i18nState.get(\"deepseek_api_key_missing\")\n                                            LLMProvider.GEMINI -> i18nState.get(\"gemini_api_key_missing\")\n                                        }\n                                    }\n                                    return@launch\n                                }\n                                \n                                val updated = llmServiceManager.applyInstructionWithLLM(\n                                    provider = selectedProvider,\n                                    session = session,\n                                    instruction = inputText,\n                                    apiKey = apiKey\n                                )\n\n                                if (updated!=null) {\n                                    // 记录本次使用的 LLM 提供商\n                                    session.lastUsedProvider = selectedProvider\n                                    onConfirm.invoke(updated)\n                                    onDismissRequest()\n                                    inputText = \"\"\n                                }\n                            } catch (e: Exception) {\n                                e.printStackTrace()\n                                withContext(Dispatchers.Main) {\n                                    errorMessage = i18nState.get(\"request_failed\") + (e.message ?: i18nState.get(\"unknown_error\"))\n                                }\n                            } finally {\n                                loading = false\n                            }\n                        }\n                    },\n                    enabled = inputText.isNotBlank() && !loading && hasApiKey\n                ) {\n                    Text(i18nState.get(\"confirm\"))\n                }\n            },\n            dismissButton = {\n                TextButton(onClick = onDismissRequest) {\n                    Text(i18nState.get(\"cancel\"))\n                }\n            }\n        )\n    }\n}\n\n@Composable\nprivate fun formatSettingsDiff(settings: ColorCorrectionSettings, i18nState: cn.netdiscovery.monica.i18n.StringResource): String {\n    val list = mutableListOf<String>()\n    if (settings.status == 1) list.add(\"${i18nState.get(\"contrast\")} → ${settings.contrast}\")\n    if (settings.status == 2) list.add(\"${i18nState.get(\"hue\")} → ${settings.hue}\")\n    if (settings.status == 3) list.add(\"${i18nState.get(\"saturation\")} → ${settings.saturation}\")\n    if (settings.status == 4) list.add(\"${i18nState.get(\"lightness\")} → ${settings.lightness}\")\n    if (settings.status == 5) list.add(\"${i18nState.get(\"temperature\")} → ${settings.temperature}\")\n    if (settings.status == 6) list.add(\"${i18nState.get(\"highlight\")} → ${settings.highlight}\")\n    if (settings.status == 7) list.add(\"${i18nState.get(\"shadow\")} → ${settings.shadow}\")\n    if (settings.status == 8) list.add(\"${i18nState.get(\"sharpen\")} → ${settings.sharpen}\")\n    if (settings.status == 9) list.add(\"${i18nState.get(\"corner\")} → ${settings.corner}\")\n    return if (list.isEmpty()) i18nState.get(\"no_significant_changes\") else list.joinToString()\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/colorpick/ColorPickView.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.colorpick\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.toComposeImageBitmap\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.material.MaterialTheme\nimport androidx.compose.material.Text\nimport cn.netdiscovery.monica.state.ApplicationState\nimport cn.netdiscovery.monica.ui.controlpanel.colorpick.model.ColorData\nimport cn.netdiscovery.monica.ui.controlpanel.colorpick.model.ColorNameParser\nimport cn.netdiscovery.monica.ui.controlpanel.colorpick.widget.ColorDisplay\nimport cn.netdiscovery.monica.ui.controlpanel.colorpick.widget.ImageColorDetector\nimport cn.netdiscovery.monica.ui.widget.image.ImageSizeCalculator\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.colorpick.ColorPickView\n * @author: Tony Shen\n * @date: 2024/6/13 16:29\n * @version: V1.0 <描述当前版本功能>\n */\n\nval colorNameParser = ColorNameParser()\n\nval defaultThumbnailSize = 150.dp\n\ntypealias OnColorChange = (ColorData) -> Unit\n\n@Composable\nfun colorPick(state: ApplicationState) {\n\n    var colorData by remember { mutableStateOf(ColorData(color = Color.Unspecified, name = \"\"))  }\n\n    // 安全获取图片，避免空指针异常\n    val image = state.currentImage?.toComposeImageBitmap()\n\n    // 如果图片为空，显示提示信息\n    if (image == null) {\n    Box(\n            Modifier.fillMaxSize(),\n            contentAlignment = Alignment.Center\n        ) {\n            Text(\n                text = \"请先加载图片\",\n                color = androidx.compose.ui.graphics.Color.Gray\n            )\n        }\n        return\n    }\n\n    Box(\n        modifier = Modifier\n            .fillMaxSize()\n            .verticalScroll(rememberScrollState())\n            .background(\n                brush = Brush.verticalGradient(\n                    colors = listOf(\n                        MaterialTheme.colors.background,\n                        MaterialTheme.colors.surface\n                    )\n                )\n            ),\n        contentAlignment = Alignment.Center\n    ) {\n        // 使用统一的图片尺寸计算\n        val (width, height) = ImageSizeCalculator.calculateImageSize(state)\n        \n        ImageColorDetector(\n            modifier = Modifier\n                .width(width)\n                .height(height),\n            contentScale = ContentScale.Fit,\n            colorNameParser = colorNameParser,\n            imageBitmap = image,\n            thumbnailSize = defaultThumbnailSize\n        ) {\n            colorData = it\n        }\n\n        if (colorData.color != Color.Unspecified) {\n            ColorDisplay(\n                modifier = Modifier.align(Alignment.CenterEnd).padding(end = 20.dp),\n                colorData = colorData\n            )\n        }\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/colorpick/model/ColorData.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.colorpick.model\n\nimport androidx.compose.ui.graphics.Color\nimport cn.netdiscovery.monica.ui.controlpanel.colorpick.utils.*\nimport kotlin.math.roundToInt\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.colorpick.model.ColorData\n * @author: Tony Shen\n * @date: 2024/6/13 20:27\n * @version: V1.0 <描述当前版本功能>\n */\ndata class ColorData(val color: Color, val name: String) {\n\n    val hexText: String\n        get() = color.toHex()\n\n    val hslString: String\n        get() {\n            val arr: FloatArray = color.toHSL()\n            return try {\n                \"H: ${arr[0].roundToInt()}° \" +\n                        \"S: ${arr[1].fractionToIntPercent()}% L: ${arr[2].fractionToIntPercent()}%\"\n            } catch (e:Exception) {\n                \"\"\n            }\n        }\n\n    val hsvString: String\n        get() {\n            val arr: FloatArray = color.toHSV()\n            return try {\n                \"H: ${arr[0].roundToInt()}° \" +\n                        \"S: ${arr[1].fractionToIntPercent()}% V: ${arr[2].fractionToIntPercent()}%\"\n            } catch (e:Exception) {\n                \"\"\n            }\n        }\n\n    val rgb: String\n        get() {\n            val rgb = color.toRGBArray()\n            return \"R: ${rgb[0]}, G: ${rgb[1]}, B: ${rgb[2]}\"\n        }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/colorpick/model/ColorNameMap.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.colorpick.model\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.colorpick.ColorNameMap\n * @author: Tony Shen\n * @date: 2024/6/13 17:08\n * @version: V1.0 <描述当前版本功能>\n */\nval colorNameMap by lazy {\n    mapOf(\n        \"#FF000000\" to \"Black\",\n        \"#FF000080\" to \"Navy Blue\",\n        \"#FF0000C8\" to \"Dark Blue\",\n        \"#FF0000FF\" to \"Blue\",\n        \"#FF000741\" to \"Stratos\",\n        \"#FF001B1C\" to \"Swamp\",\n        \"#FF002387\" to \"Resolution Blue\",\n        \"#FF002900\" to \"Deep Fir\",\n        \"#FF002E20\" to \"Burnham\",\n        \"#FF002FA7\" to \"Klein Blue\",\n        \"#FF003153\" to \"Prussian Blue\",\n        \"#FF003366\" to \"Midnight Blue\",\n        \"#FF003399\" to \"Smalt\",\n        \"#FF003532\" to \"Deep Teal\",\n        \"#FF003E40\" to \"Cyprus\",\n        \"#FF004620\" to \"Kaitoke Green\",\n        \"#FF0047AB\" to \"Cobalt\",\n        \"#FF004816\" to \"Crusoe\",\n        \"#FF004950\" to \"Sherpa Blue\",\n        \"#FF0056A7\" to \"Endeavour\",\n        \"#FF00581A\" to \"Camarone\",\n        \"#FF0066CC\" to \"Science Blue\",\n        \"#FF0066FF\" to \"Blue Ribbon\",\n        \"#FF00755E\" to \"Tropical Rain Forest\",\n        \"#FF0076A3\" to \"Allports\",\n        \"#FF007BA7\" to \"Deep Cerulean\",\n        \"#FF007EC7\" to \"Lochmara\",\n        \"#FF007FFF\" to \"Azure Radiance\",\n        \"#FF008080\" to \"Teal\",\n        \"#FF0095B6\" to \"Bondi Blue\",\n        \"#FF009DC4\" to \"Pacific Blue\",\n        \"#FF00A693\" to \"Persian Green\",\n        \"#FF00A86B\" to \"Jade\",\n        \"#FF00CC99\" to \"Caribbean Green\",\n        \"#FF00CCCC\" to \"Robin's Egg Blue\",\n        \"#FF00FF00\" to \"Green\",\n        \"#FF00FF7F\" to \"Spring Green\",\n        \"#FF00FFFF\" to \"Cyan Aqua\",\n        \"#FF010D1A\" to \"Blue Charcoal\",\n        \"#FF011635\" to \"Midnight\",\n        \"#FF011D13\" to \"Holly\",\n        \"#FF012731\" to \"Daintree\",\n        \"#FF01361C\" to \"Cardin Green\",\n        \"#FF01371A\" to \"County Green\",\n        \"#FF013E62\" to \"Astronaut Blue\",\n        \"#FF013F6A\" to \"Regal Blue\",\n        \"#FF014B43\" to \"Aqua Deep\",\n        \"#FF015E85\" to \"Orient\",\n        \"#FF016162\" to \"Blue Stone\",\n        \"#FF016D39\" to \"Fun Green\",\n        \"#FF01796F\" to \"Pine Green\",\n        \"#FF017987\" to \"Blue Lagoon\",\n        \"#FF01826B\" to \"Deep Sea\",\n        \"#FF01A368\" to \"Green Haze\",\n        \"#FF022D15\" to \"English Holly\",\n        \"#FF02402C\" to \"Sherwood Green\",\n        \"#FF02478E\" to \"Congress Blue\",\n        \"#FF024E46\" to \"Evening Sea\",\n        \"#FF026395\" to \"Bahama Blue\",\n        \"#FF02866F\" to \"Observatory\",\n        \"#FF02A4D3\" to \"Cerulean\",\n        \"#FF03163C\" to \"Tangaroa\",\n        \"#FF032B52\" to \"Green Vogue\",\n        \"#FF036A6E\" to \"Mosque\",\n        \"#FF041004\" to \"Midnight Moss\",\n        \"#FF041322\" to \"Black Pearl\",\n        \"#FF042E4C\" to \"Blue Whale\",\n        \"#FF044022\" to \"Zuccini\",\n        \"#FF044259\" to \"Teal Blue\",\n        \"#FF051040\" to \"Deep Cove\",\n        \"#FF051657\" to \"Gulf Blue\",\n        \"#FF055989\" to \"Venice Blue\",\n        \"#FF056F57\" to \"Watercourse\",\n        \"#FF062A78\" to \"Catalina Blue\",\n        \"#FF063537\" to \"Tiber\",\n        \"#FF069B81\" to \"Gossamer\",\n        \"#FF06A189\" to \"Niagara\",\n        \"#FF073A50\" to \"Tarawera\",\n        \"#FF080110\" to \"Jaguar\",\n        \"#FF081910\" to \"Black Bean\",\n        \"#FF082567\" to \"Deep Sapphire\",\n        \"#FF088370\" to \"Elf Green\",\n        \"#FF08E8DE\" to \"Bright Turquoise\",\n        \"#FF092256\" to \"Downriver\",\n        \"#FF09230F\" to \"Palm Green\",\n        \"#FF09255D\" to \"Madison\",\n        \"#FF093624\" to \"Bottle Green\",\n        \"#FF095859\" to \"Deep Sea Green\",\n        \"#FF097F4B\" to \"Salem\",\n        \"#FF0A001C\" to \"Black Russian\",\n        \"#FF0A480D\" to \"Dark Fern\",\n        \"#FF0A6906\" to \"Japanese Laurel\",\n        \"#FF0A6F75\" to \"Atoll\",\n        \"#FF0B0B0B\" to \"Cod Gray\",\n        \"#FF0B0F08\" to \"Marshland\",\n        \"#FF0B1107\" to \"Gordons Green\",\n        \"#FF0B1304\" to \"Black Forest\",\n        \"#FF0B6207\" to \"San Felix\",\n        \"#FF0BDA51\" to \"Malachite\",\n        \"#FF0C0B1D\" to \"Ebony\",\n        \"#FF0C0D0F\" to \"Woodsmoke\",\n        \"#FF0C1911\" to \"Racing Green\",\n        \"#FF0C7A79\" to \"Surfie Green\",\n        \"#FF0C8990\" to \"Blue Chill\",\n        \"#FF0D0332\" to \"Black Rock\",\n        \"#FF0D1117\" to \"Bunker\",\n        \"#FF0D1C19\" to \"Aztec\",\n        \"#FF0D2E1C\" to \"Bush\",\n        \"#FF0E0E18\" to \"Cinder\",\n        \"#FF0E2A30\" to \"Firefly\",\n        \"#FF0F2D9E\" to \"Torea Bay\",\n        \"#FF10121D\" to \"Vulcan\",\n        \"#FF101405\" to \"Green Waterloo\",\n        \"#FF105852\" to \"Eden\",\n        \"#FF110C6C\" to \"Arapawa\",\n        \"#FF120A8F\" to \"Ultramarine\",\n        \"#FF123447\" to \"Elephant\",\n        \"#FF126B40\" to \"Jewel\",\n        \"#FF130000\" to \"Diesel\",\n        \"#FF130A06\" to \"Asphalt\",\n        \"#FF13264D\" to \"Blue Zodiac\",\n        \"#FF134F19\" to \"Parsley\",\n        \"#FF140600\" to \"Nero\",\n        \"#FF1450AA\" to \"Tory Blue\",\n        \"#FF151F4C\" to \"Bunting\",\n        \"#FF1560BD\" to \"Denim\",\n        \"#FF15736B\" to \"Genoa\",\n        \"#FF161928\" to \"Mirage\",\n        \"#FF161D10\" to \"Hunter Green\",\n        \"#FF162A40\" to \"Big Stone\",\n        \"#FF163222\" to \"Celtic\",\n        \"#FF16322C\" to \"Timber Green\",\n        \"#FF163531\" to \"Gable Green\",\n        \"#FF171F04\" to \"Pine Tree\",\n        \"#FF175579\" to \"Chathams Blue\",\n        \"#FF182D09\" to \"Deep Forest Green\",\n        \"#FF18587A\" to \"Blumine\",\n        \"#FF19330E\" to \"Palm Leaf\",\n        \"#FF193751\" to \"Nile Blue\",\n        \"#FF1959A8\" to \"Fun Blue\",\n        \"#FF1A1A68\" to \"Lucky Point\",\n        \"#FF1AB385\" to \"Mountain Meadow\",\n        \"#FF1B0245\" to \"Tolopea\",\n        \"#FF1B1035\" to \"Haiti\",\n        \"#FF1B127B\" to \"Deep Koamaru\",\n        \"#FF1B1404\" to \"Acadia\",\n        \"#FF1B2F11\" to \"Seaweed\",\n        \"#FF1B3162\" to \"Biscay\",\n        \"#FF1B659D\" to \"Matisse\",\n        \"#FF1C1208\" to \"Crowshead\",\n        \"#FF1C1E13\" to \"Rangoon Green\",\n        \"#FF1C39BB\" to \"Persian Blue\",\n        \"#FF1C402E\" to \"Everglade\",\n        \"#FF1C7C7D\" to \"Elm\",\n        \"#FF1D6142\" to \"Green Pea\",\n        \"#FF1E0F04\" to \"Creole\",\n        \"#FF1E1609\" to \"Karaka\",\n        \"#FF1E1708\" to \"El Paso\",\n        \"#FF1E385B\" to \"Cello\",\n        \"#FF1E433C\" to \"Te Papa Green\",\n        \"#FF1E90FF\" to \"Dodger Blue\",\n        \"#FF1E9AB0\" to \"Eastern Blue\",\n        \"#FF1F120F\" to \"Night Rider\",\n        \"#FF1FC2C2\" to \"Java\",\n        \"#FF20208D\" to \"Jacksons Purple\",\n        \"#FF202E54\" to \"Cloud Burst\",\n        \"#FF204852\" to \"Blue Dianne\",\n        \"#FF211A0E\" to \"Eternity\",\n        \"#FF220878\" to \"Deep Blue\",\n        \"#FF228B22\" to \"Forest Green\",\n        \"#FF233418\" to \"Mallard\",\n        \"#FF240A40\" to \"Violet\",\n        \"#FF240C02\" to \"Kilimanjaro\",\n        \"#FF242A1D\" to \"Log Cabin\",\n        \"#FF242E16\" to \"Black Olive\",\n        \"#FF24500F\" to \"Green House\",\n        \"#FF251607\" to \"Graphite\",\n        \"#FF251706\" to \"Cannon Black\",\n        \"#FF251F4F\" to \"Port Gore\",\n        \"#FF25272C\" to \"Shark\",\n        \"#FF25311C\" to \"Green Kelp\",\n        \"#FF2596D1\" to \"Curious Blue\",\n        \"#FF260368\" to \"Paua\",\n        \"#FF26056A\" to \"Paris M\",\n        \"#FF261105\" to \"Wood Bark\",\n        \"#FF261414\" to \"Gondola\",\n        \"#FF262335\" to \"Steel Gray\",\n        \"#FF26283B\" to \"Ebony Clay\",\n        \"#FF273A81\" to \"Bay of Many\",\n        \"#FF27504B\" to \"Plantation\",\n        \"#FF278A5B\" to \"Eucalyptus\",\n        \"#FF281E15\" to \"Oil\",\n        \"#FF283A77\" to \"Astronaut\",\n        \"#FF286ACD\" to \"Mariner\",\n        \"#FF290C5E\" to \"Violent Violet\",\n        \"#FF292130\" to \"Bastille\",\n        \"#FF292319\" to \"Zeus\",\n        \"#FF292937\" to \"Charade\",\n        \"#FF297B9A\" to \"Jelly Bean\",\n        \"#FF29AB87\" to \"Jungle Green\",\n        \"#FF2A0359\" to \"Cherry Pie\",\n        \"#FF2A140E\" to \"Coffee Bean\",\n        \"#FF2A2630\" to \"Baltic Sea\",\n        \"#FF2A380B\" to \"Turtle Green\",\n        \"#FF2A52BE\" to \"Cerulean Blue\",\n        \"#FF2B0202\" to \"Sepia Black\",\n        \"#FF2B194F\" to \"Valhalla\",\n        \"#FF2B3228\" to \"Heavy Metal\",\n        \"#FF2C0E8C\" to \"Blue Gem\",\n        \"#FF2C1632\" to \"Revolver\",\n        \"#FF2C2133\" to \"Bleached Cedar\",\n        \"#FF2C8C84\" to \"Lochinvar\",\n        \"#FF2D2510\" to \"Mikado\",\n        \"#FF2D383A\" to \"Outer Space\",\n        \"#FF2D569B\" to \"St Tropez\",\n        \"#FF2E0329\" to \"Jacaranda\",\n        \"#FF2E1905\" to \"Jacko Bean\",\n        \"#FF2E3222\" to \"Rangitoto\",\n        \"#FF2E3F62\" to \"Rhino\",\n        \"#FF2E8B57\" to \"Sea Green\",\n        \"#FF2EBFD4\" to \"Scooter\",\n        \"#FF2F270E\" to \"Onion\",\n        \"#FF2F3CB3\" to \"Governor Bay\",\n        \"#FF2F519E\" to \"Sapphire\",\n        \"#FF2F5A57\" to \"Spectra\",\n        \"#FF2F6168\" to \"Casal\",\n        \"#FF300529\" to \"Melanzane\",\n        \"#FF301F1E\" to \"Cocoa Brown\",\n        \"#FF302A0F\" to \"Woodrush\",\n        \"#FF304B6A\" to \"San Juan\",\n        \"#FF30D5C8\" to \"Turquoise\",\n        \"#FF311C17\" to \"Eclipse\",\n        \"#FF314459\" to \"Pickled Bluewood\",\n        \"#FF315BA1\" to \"Azure\",\n        \"#FF31728D\" to \"Calypso\",\n        \"#FF317D82\" to \"Paradiso\",\n        \"#FF32127A\" to \"Persian Indigo\",\n        \"#FF32293A\" to \"Blackcurrant\",\n        \"#FF323232\" to \"Mine Shaft\",\n        \"#FF325D52\" to \"Stromboli\",\n        \"#FF327C14\" to \"Bilbao\",\n        \"#FF327DA0\" to \"Astral\",\n        \"#FF33036B\" to \"Christalle\",\n        \"#FF33292F\" to \"Thunder\",\n        \"#FF33CC99\" to \"Shamrock\",\n        \"#FF341515\" to \"Tamarind\",\n        \"#FF350036\" to \"Mardi Gras\",\n        \"#FF350E42\" to \"Valentino\",\n        \"#FF350E57\" to \"Jagger\",\n        \"#FF353542\" to \"Tuna\",\n        \"#FF354E8C\" to \"Chambray\",\n        \"#FF363050\" to \"Martinique\",\n        \"#FF363534\" to \"Tuatara\",\n        \"#FF363C0D\" to \"Waiouru\",\n        \"#FF36747D\" to \"Ming\",\n        \"#FF368716\" to \"La Palma\",\n        \"#FF370202\" to \"Chocolate\",\n        \"#FF371D09\" to \"Clinker\",\n        \"#FF37290E\" to \"Brown Tumbleweed\",\n        \"#FF373021\" to \"Birch\",\n        \"#FF377475\" to \"Oracle\",\n        \"#FF380474\" to \"Blue Diamond\",\n        \"#FF381A51\" to \"Grape\",\n        \"#FF383533\" to \"Dune\",\n        \"#FF384555\" to \"Oxford Blue\",\n        \"#FF384910\" to \"Clover\",\n        \"#FF394851\" to \"Limed Spruce\",\n        \"#FF396413\" to \"Dell\",\n        \"#FF3A0020\" to \"Toledo\",\n        \"#FF3A2010\" to \"Sambuca\",\n        \"#FF3A2A6A\" to \"Jacarta\",\n        \"#FF3A686C\" to \"William\",\n        \"#FF3A6A47\" to \"Killarney\",\n        \"#FF3AB09E\" to \"Keppel\",\n        \"#FF3B000B\" to \"Temptress\",\n        \"#FF3B0910\" to \"Aubergine\",\n        \"#FF3B1F1F\" to \"Jon\",\n        \"#FF3B2820\" to \"Treehouse\",\n        \"#FF3B7A57\" to \"Amazon\",\n        \"#FF3B91B4\" to \"Boston Blue\",\n        \"#FF3C0878\" to \"Windsor\",\n        \"#FF3C1206\" to \"Rebel\",\n        \"#FF3C1F76\" to \"Meteorite\",\n        \"#FF3C2005\" to \"Dark Ebony\",\n        \"#FF3C3910\" to \"Camouflage\",\n        \"#FF3C4151\" to \"Bright Gray\",\n        \"#FF3C4443\" to \"Cape Cod\",\n        \"#FF3C493A\" to \"Lunar Green\",\n        \"#FF3D0C02\" to \"Bean  \",\n        \"#FF3D2B1F\" to \"Bistre\",\n        \"#FF3D7D52\" to \"Goblin\",\n        \"#FF3E0480\" to \"Kingfisher Daisy\",\n        \"#FF3E1C14\" to \"Cedar\",\n        \"#FF3E2B23\" to \"English Walnut\",\n        \"#FF3E2C1C\" to \"Black Marlin\",\n        \"#FF3E3A44\" to \"Ship Gray\",\n        \"#FF3EABBF\" to \"Pelorous\",\n        \"#FF3F2109\" to \"Bronze\",\n        \"#FF3F2500\" to \"Cola\",\n        \"#FF3F3002\" to \"Madras\",\n        \"#FF3F307F\" to \"Minsk\",\n        \"#FF3F4C3A\" to \"Cabbage Pont\",\n        \"#FF3F583B\" to \"Tom Thumb\",\n        \"#FF3F5D53\" to \"Mineral Green\",\n        \"#FF3FC1AA\" to \"Puerto Rico\",\n        \"#FF3FFF00\" to \"Harlequin\",\n        \"#FF401801\" to \"Brown Pod\",\n        \"#FF40291D\" to \"Cork\",\n        \"#FF403B38\" to \"Masala\",\n        \"#FF403D19\" to \"Thatch Green\",\n        \"#FF405169\" to \"Fjord\",\n        \"#FF40826D\" to \"Viridian\",\n        \"#FF40A860\" to \"Chateau Green\",\n        \"#FF410056\" to \"Ripe Plum\",\n        \"#FF411E10\" to \"Paco\",\n        \"#FF412010\" to \"Deep Oak\",\n        \"#FF413C37\" to \"Merlin\",\n        \"#FF414257\" to \"Gun Powder\",\n        \"#FF414C7D\" to \"East Bay\",\n        \"#FF4169E1\" to \"Royal Blue\",\n        \"#FF41AA78\" to \"Ocean Green\",\n        \"#FF420303\" to \"Burnt Maroon\",\n        \"#FF423921\" to \"Lisbon Brown\",\n        \"#FF427977\" to \"Faded Jade\",\n        \"#FF431560\" to \"Scarlet Gum\",\n        \"#FF433120\" to \"Iroko\",\n        \"#FF433E37\" to \"Armadillo\",\n        \"#FF434C59\" to \"River Bed\",\n        \"#FF436A0D\" to \"Green Leaf\",\n        \"#FF44012D\" to \"Barossa\",\n        \"#FF441D00\" to \"Morocco Brown\",\n        \"#FF444954\" to \"Mako\",\n        \"#FF454936\" to \"Kelp\",\n        \"#FF456CAC\" to \"San Marino\",\n        \"#FF45B1E8\" to \"Picton Blue\",\n        \"#FF460B41\" to \"Loulou\",\n        \"#FF462425\" to \"Crater Brown\",\n        \"#FF465945\" to \"Gray Asparagus\",\n        \"#FF4682B4\" to \"Steel Blue\",\n        \"#FF480404\" to \"Rustic Red\",\n        \"#FF480607\" to \"Bulgarian Rose\",\n        \"#FF480656\" to \"Clairvoyant\",\n        \"#FF481C1C\" to \"Cocoa Bean\",\n        \"#FF483131\" to \"Woody Brown\",\n        \"#FF483C32\" to \"Taupe\",\n        \"#FF49170C\" to \"Van Cleef\",\n        \"#FF492615\" to \"Brown Derby\",\n        \"#FF49371B\" to \"Metallic Bronze\",\n        \"#FF495400\" to \"Verdun Green\",\n        \"#FF496679\" to \"Blue Bayoux\",\n        \"#FF497183\" to \"Bismark\",\n        \"#FF4A2A04\" to \"Bracken\",\n        \"#FF4A3004\" to \"Deep Bronze\",\n        \"#FF4A3C30\" to \"Mondo\",\n        \"#FF4A4244\" to \"Tundora\",\n        \"#FF4A444B\" to \"Gravel\",\n        \"#FF4A4E5A\" to \"Trout\",\n        \"#FF4B0082\" to \"Pigment Indigo\",\n        \"#FF4B5D52\" to \"Nandor\",\n        \"#FF4C3024\" to \"Saddle\",\n        \"#FF4C4F56\" to \"Abbey\",\n        \"#FF4D0135\" to \"Blackberry\",\n        \"#FF4D0A18\" to \"Cab Sav\",\n        \"#FF4D1E01\" to \"Indian Tan\",\n        \"#FF4D282D\" to \"Cowboy\",\n        \"#FF4D282E\" to \"Livid Brown\",\n        \"#FF4D3833\" to \"Rock\",\n        \"#FF4D3D14\" to \"Punga\",\n        \"#FF4D400F\" to \"Bronzetone\",\n        \"#FF4D5328\" to \"Woodland\",\n        \"#FF4E0606\" to \"Mahogany\",\n        \"#FF4E2A5A\" to \"Bossanova\",\n        \"#FF4E3B41\" to \"Matterhorn\",\n        \"#FF4E420C\" to \"Bronze Olive\",\n        \"#FF4E4562\" to \"Mulled Wine\",\n        \"#FF4E6649\" to \"Axolotl\",\n        \"#FF4E7F9E\" to \"Wedgewood\",\n        \"#FF4EABD1\" to \"Shakespeare\",\n        \"#FF4F1C70\" to \"Honey Flower\",\n        \"#FF4F2398\" to \"Daisy Bush\",\n        \"#FF4F69C6\" to \"Indigo\",\n        \"#FF4F7942\" to \"Fern Green\",\n        \"#FF4F9D5D\" to \"Fruit Salad\",\n        \"#FF4FA83D\" to \"Apple\",\n        \"#FF504351\" to \"Mortar\",\n        \"#FF507096\" to \"Kashmir Blue\",\n        \"#FF507672\" to \"Cutty Sark\",\n        \"#FF50C878\" to \"Emerald\",\n        \"#FF514649\" to \"Emperor\",\n        \"#FF516E3D\" to \"Chalet Green\",\n        \"#FF517C66\" to \"Como\",\n        \"#FF51808F\" to \"Smalt Blue\",\n        \"#FF52001F\" to \"Castro\",\n        \"#FF520C17\" to \"Maroon Oak\",\n        \"#FF523C94\" to \"Gigas\",\n        \"#FF533455\" to \"Voodoo\",\n        \"#FF534491\" to \"Victoria\",\n        \"#FF53824B\" to \"Hippie Green\",\n        \"#FF541012\" to \"Heath\",\n        \"#FF544333\" to \"Judge Gray\",\n        \"#FF54534D\" to \"Fuscous Gray\",\n        \"#FF549019\" to \"Vida Loca\",\n        \"#FF55280C\" to \"Cioccolato\",\n        \"#FF555B10\" to \"Saratoga\",\n        \"#FF556D56\" to \"Finlandia\",\n        \"#FF5590D9\" to \"Havelock Blue\",\n        \"#FF56B4BE\" to \"Fountain Blue\",\n        \"#FF578363\" to \"Spring Leaves\",\n        \"#FF583401\" to \"Saddle Brown\",\n        \"#FF585562\" to \"Scarpa Flow\",\n        \"#FF587156\" to \"Cactus\",\n        \"#FF589AAF\" to \"Hippie Blue\",\n        \"#FF591D35\" to \"Wine Berry\",\n        \"#FF592804\" to \"Brown Bramble\",\n        \"#FF593737\" to \"Congo Brown\",\n        \"#FF594433\" to \"Millbrook\",\n        \"#FF5A6E9C\" to \"Waikawa Gray\",\n        \"#FF5A87A0\" to \"Horizon\",\n        \"#FF5B3013\" to \"Jambalaya\",\n        \"#FF5C0120\" to \"Bordeaux\",\n        \"#FF5C0536\" to \"Mulberry Wood\",\n        \"#FF5C2E01\" to \"Carnaby Tan\",\n        \"#FF5C5D75\" to \"Comet\",\n        \"#FF5D1E0F\" to \"Redwood\",\n        \"#FF5D4C51\" to \"Don Juan\",\n        \"#FF5D5C58\" to \"Chicago\",\n        \"#FF5D5E37\" to \"Verdigris\",\n        \"#FF5D7747\" to \"Dingley\",\n        \"#FF5DA19F\" to \"Breaker Bay\",\n        \"#FF5E483E\" to \"Kabul\",\n        \"#FF5E5D3B\" to \"Hemlock\",\n        \"#FF5F3D26\" to \"Irish Coffee\",\n        \"#FF5F5F6E\" to \"Mid Gray\",\n        \"#FF5F6672\" to \"Shuttle Gray\",\n        \"#FF5FA777\" to \"Aqua Forest\",\n        \"#FF5FB3AC\" to \"Tradewind\",\n        \"#FF604913\" to \"Horses Neck\",\n        \"#FF605B73\" to \"Smoky\",\n        \"#FF606E68\" to \"Corduroy\",\n        \"#FF6093D1\" to \"Danube\",\n        \"#FF612718\" to \"Espresso\",\n        \"#FF614051\" to \"Eggplant\",\n        \"#FF615D30\" to \"Costa Del Sol\",\n        \"#FF61845F\" to \"Glade Green\",\n        \"#FF622F30\" to \"Buccaneer\",\n        \"#FF623F2D\" to \"Quincy\",\n        \"#FF624E9A\" to \"Butterfly Bush\",\n        \"#FF625119\" to \"West Coast\",\n        \"#FF626649\" to \"Finch\",\n        \"#FF639A8F\" to \"Patina\",\n        \"#FF63B76C\" to \"Fern\",\n        \"#FF6456B7\" to \"Blue Violet\",\n        \"#FF646077\" to \"Dolphin\",\n        \"#FF646463\" to \"Storm Dust\",\n        \"#FF646A54\" to \"Siam\",\n        \"#FF646E75\" to \"Nevada\",\n        \"#FF6495ED\" to \"Cornflower Blue\",\n        \"#FF64CCDB\" to \"Viking\",\n        \"#FF65000B\" to \"Rosewood\",\n        \"#FF651A14\" to \"Cherrywood\",\n        \"#FF652DC1\" to \"Purple Heart\",\n        \"#FF657220\" to \"Fern Frond\",\n        \"#FF65745D\" to \"Willow Grove\",\n        \"#FF65869F\" to \"Hoki\",\n        \"#FF660045\" to \"Pompadour\",\n        \"#FF660099\" to \"Purple\",\n        \"#FF66023C\" to \"Tyrian Purple\",\n        \"#FF661010\" to \"Dark Tan\",\n        \"#FF66B58F\" to \"Silver Tree\",\n        \"#FF66FF00\" to \"Bright Green\",\n        \"#FF66FF66\" to \"Screamin Green\",\n        \"#FF67032D\" to \"Black Rose\",\n        \"#FF675FA6\" to \"Scampi\",\n        \"#FF676662\" to \"Ironside Gray\",\n        \"#FF678975\" to \"Viridian Green\",\n        \"#FF67A712\" to \"Christi\",\n        \"#FF683600\" to \"Nutmeg Wood Finish\",\n        \"#FF685558\" to \"Zambezi\",\n        \"#FF685E6E\" to \"Salt Box\",\n        \"#FF692545\" to \"Tawny Port\",\n        \"#FF692D54\" to \"Finn\",\n        \"#FF695F62\" to \"Scorpion\",\n        \"#FF697E9A\" to \"Lynch\",\n        \"#FF6A442E\" to \"Spice\",\n        \"#FF6A5D1B\" to \"Himalaya\",\n        \"#FF6A6051\" to \"Soya Bean\",\n        \"#FF6B2A14\" to \"Hairy Heath\",\n        \"#FF6B3FA0\" to \"Royal Purple\",\n        \"#FF6B4E31\" to \"Shingle Fawn\",\n        \"#FF6B5755\" to \"Dorado\",\n        \"#FF6B8BA2\" to \"Bermuda Gray\",\n        \"#FF6B8E23\" to \"Olive Drab\",\n        \"#FF6C3082\" to \"Eminence\",\n        \"#FF6CDAE7\" to \"Turquoise Blue\",\n        \"#FF6D0101\" to \"Lonestar\",\n        \"#FF6D5E54\" to \"Pine Cone\",\n        \"#FF6D6C6C\" to \"Dove Gray\",\n        \"#FF6D9292\" to \"Juniper\",\n        \"#FF6D92A1\" to \"Gothic\",\n        \"#FF6E0902\" to \"Red Oxide\",\n        \"#FF6E1D14\" to \"Moccaccino\",\n        \"#FF6E4826\" to \"Pickled Bean\",\n        \"#FF6E4B26\" to \"Dallas\",\n        \"#FF6E6D57\" to \"Kokoda\",\n        \"#FF6E7783\" to \"Pale Sky\",\n        \"#FF6F440C\" to \"Cafe Royale\",\n        \"#FF6F6A61\" to \"Flint\",\n        \"#FF6F8E63\" to \"Highland\",\n        \"#FF6F9D02\" to \"Limeade\",\n        \"#FF6FD0C5\" to \"Downy\",\n        \"#FF701C1C\" to \"Persian Plum\",\n        \"#FF704214\" to \"Sepia\",\n        \"#FF704A07\" to \"Antique Bronze\",\n        \"#FF704F50\" to \"Ferra\",\n        \"#FF706555\" to \"Coffee\",\n        \"#FF708090\" to \"Slate Gray\",\n        \"#FF711A00\" to \"Cedar Wood Finish\",\n        \"#FF71291D\" to \"Metallic Copper\",\n        \"#FF714693\" to \"Affair\",\n        \"#FF714AB2\" to \"Studio\",\n        \"#FF715D47\" to \"Tobacco Brown\",\n        \"#FF716338\" to \"Yellow Metal\",\n        \"#FF716B56\" to \"Peat\",\n        \"#FF716E10\" to \"Olivetone\",\n        \"#FF717486\" to \"Storm Gray\",\n        \"#FF718080\" to \"Sirocco\",\n        \"#FF71D9E2\" to \"Aquamarine Blue\",\n        \"#FF72010F\" to \"Venetian Red\",\n        \"#FF724A2F\" to \"Old Copper\",\n        \"#FF726D4E\" to \"Go Ben\",\n        \"#FF727B89\" to \"Raven\",\n        \"#FF731E8F\" to \"Seance\",\n        \"#FF734A12\" to \"Raw Umber\",\n        \"#FF736C9F\" to \"Kimberly\",\n        \"#FF736D58\" to \"Crocodile\",\n        \"#FF737829\" to \"Crete\",\n        \"#FF738678\" to \"Xanadu\",\n        \"#FF74640D\" to \"Spicy Mustard\",\n        \"#FF747D63\" to \"Limed Ash\",\n        \"#FF747D83\" to \"Rolling Stone\",\n        \"#FF748881\" to \"Blue Smoke\",\n        \"#FF749378\" to \"Laurel\",\n        \"#FF74C365\" to \"Mantis\",\n        \"#FF755A57\" to \"Russett\",\n        \"#FF7563A8\" to \"Deluge\",\n        \"#FF76395D\" to \"Cosmic\",\n        \"#FF7666C6\" to \"Blue Marguerite\",\n        \"#FF76BD17\" to \"Lima\",\n        \"#FF76D7EA\" to \"Sky Blue\",\n        \"#FF770F05\" to \"Dark Burgundy\",\n        \"#FF771F1F\" to \"Crown of Thorns\",\n        \"#FF773F1A\" to \"Walnut\",\n        \"#FF776F61\" to \"Pablo\",\n        \"#FF778120\" to \"Pacifika\",\n        \"#FF779E86\" to \"Oxley\",\n        \"#FF77DD77\" to \"Pastel Green\",\n        \"#FF780109\" to \"Japanese Maple\",\n        \"#FF782D19\" to \"Mocha\",\n        \"#FF782F16\" to \"Peanut\",\n        \"#FF78866B\" to \"Camouflage Green\",\n        \"#FF788A25\" to \"Wasabi\",\n        \"#FF788BBA\" to \"Ship Cove\",\n        \"#FF78A39C\" to \"Sea Nymph\",\n        \"#FF795D4C\" to \"Roman Coffee\",\n        \"#FF796878\" to \"Old Lavender\",\n        \"#FF796989\" to \"Rum\",\n        \"#FF796A78\" to \"Fedora\",\n        \"#FF796D62\" to \"Sandstone\",\n        \"#FF79DEEC\" to \"Spray\",\n        \"#FF7A013A\" to \"Siren\",\n        \"#FF7A58C1\" to \"Fuchsia Blue\",\n        \"#FF7A7A7A\" to \"Boulder\",\n        \"#FF7A89B8\" to \"Wild Blue Yonder\",\n        \"#FF7AC488\" to \"De York\",\n        \"#FF7B3801\" to \"Red Beech\",\n        \"#FF7B3F00\" to \"Cinnamon\",\n        \"#FF7B6608\" to \"Yukon Gold\",\n        \"#FF7B7874\" to \"Tapa\",\n        \"#FF7B7C94\" to \"Waterloo \",\n        \"#FF7B8265\" to \"Flax Smoke\",\n        \"#FF7B9F80\" to \"Amulet\",\n        \"#FF7BA05B\" to \"Asparagus\",\n        \"#FF7C1C05\" to \"Kenyan Copper\",\n        \"#FF7C7631\" to \"Pesto\",\n        \"#FF7C778A\" to \"Topaz\",\n        \"#FF7C7B7A\" to \"Concord\",\n        \"#FF7C7B82\" to \"Jumbo\",\n        \"#FF7C881A\" to \"Trendy Green\",\n        \"#FF7CA1A6\" to \"Gumbo\",\n        \"#FF7CB0A1\" to \"Acapulco\",\n        \"#FF7CB7BB\" to \"Neptune\",\n        \"#FF7D2C14\" to \"Pueblo\",\n        \"#FF7DA98D\" to \"Bay Leaf\",\n        \"#FF7DC8F7\" to \"Malibu\",\n        \"#FF7DD8C6\" to \"Bermuda\",\n        \"#FF7E3A15\" to \"Copper Canyon\",\n        \"#FF7F1734\" to \"Claret\",\n        \"#FF7F3A02\" to \"Peru Tan\",\n        \"#FF7F626D\" to \"Falcon\",\n        \"#FF7F7589\" to \"Mobster\",\n        \"#FF7F76D3\" to \"Moody Blue\",\n        \"#FF7FFF00\" to \"Chartreuse\",\n        \"#FF7FFFD4\" to \"Aquamarine\",\n        \"#FF800000\" to \"Maroon\",\n        \"#FF800B47\" to \"Rose Bud Cherry\",\n        \"#FF801818\" to \"Falu Red\",\n        \"#FF80341F\" to \"Red Robin\",\n        \"#FF803790\" to \"Vivid Violet\",\n        \"#FF80461B\" to \"Russet\",\n        \"#FF807E79\" to \"Friar Gray\",\n        \"#FF808000\" to \"Olive\",\n        \"#FF808080\" to \"Gray\",\n        \"#FF80B3AE\" to \"Gulf Stream\",\n        \"#FF80B3C4\" to \"Glacier\",\n        \"#FF80CCEA\" to \"Seagull\",\n        \"#FF81422C\" to \"Nutmeg\",\n        \"#FF816E71\" to \"Spicy Pink\",\n        \"#FF817377\" to \"Empress\",\n        \"#FF819885\" to \"Spanish Green\",\n        \"#FF826F65\" to \"Sand Dune\",\n        \"#FF828685\" to \"Gunsmoke\",\n        \"#FF828F72\" to \"Battleship Gray\",\n        \"#FF831923\" to \"Merlot\",\n        \"#FF837050\" to \"Shadow\",\n        \"#FF83AA5D\" to \"Chelsea Cucumber\",\n        \"#FF83D0C6\" to \"Monte Carlo\",\n        \"#FF843179\" to \"Plum\",\n        \"#FF84A0A0\" to \"Granny Smith\",\n        \"#FF8581D9\" to \"Chetwode Blue\",\n        \"#FF858470\" to \"Bandicoot\",\n        \"#FF859FAF\" to \"Bali Hai\",\n        \"#FF85C4CC\" to \"Half Baked\",\n        \"#FF860111\" to \"Red Devil\",\n        \"#FF863C3C\" to \"Lotus\",\n        \"#FF86483C\" to \"Ironstone\",\n        \"#FF864D1E\" to \"Bull Shot\",\n        \"#FF86560A\" to \"Rusty Nail\",\n        \"#FF868974\" to \"Bitter\",\n        \"#FF86949F\" to \"Regent Gray\",\n        \"#FF871550\" to \"Disco\",\n        \"#FF87756E\" to \"Americano\",\n        \"#FF877C7B\" to \"Hurricane\",\n        \"#FF878D91\" to \"Oslo Gray\",\n        \"#FF87AB39\" to \"Sushi\",\n        \"#FF885342\" to \"Spicy Mix\",\n        \"#FF886221\" to \"Kumera\",\n        \"#FF888387\" to \"Suva Gray\",\n        \"#FF888D65\" to \"Avocado\",\n        \"#FF893456\" to \"Camelot\",\n        \"#FF893843\" to \"Solid Pink\",\n        \"#FF894367\" to \"Cannon Pink\",\n        \"#FF897D6D\" to \"Makara\",\n        \"#FF8A3324\" to \"Burnt Umber\",\n        \"#FF8A73D6\" to \"True V\",\n        \"#FF8A8360\" to \"Clay Creek\",\n        \"#FF8A8389\" to \"Monsoon\",\n        \"#FF8A8F8A\" to \"Stack\",\n        \"#FF8AB9F1\" to \"Jordy Blue\",\n        \"#FF8B00FF\" to \"Electric Violet\",\n        \"#FF8B0723\" to \"Monarch\",\n        \"#FF8B6B0B\" to \"Corn Harvest\",\n        \"#FF8B8470\" to \"Olive Haze\",\n        \"#FF8B847E\" to \"Schooner\",\n        \"#FF8B8680\" to \"Natural Gray\",\n        \"#FF8B9C90\" to \"Mantle\",\n        \"#FF8B9FEE\" to \"Portage\",\n        \"#FF8BA690\" to \"Envy\",\n        \"#FF8BA9A5\" to \"Cascade\",\n        \"#FF8BE6D8\" to \"Riptide\",\n        \"#FF8C055E\" to \"Cardinal Pink\",\n        \"#FF8C472F\" to \"Mule Fawn\",\n        \"#FF8C5738\" to \"Potters Clay\",\n        \"#FF8C6495\" to \"Trendy Pink\",\n        \"#FF8D0226\" to \"Paprika\",\n        \"#FF8D3D38\" to \"Sanguine Brown\",\n        \"#FF8D3F3F\" to \"Tosca\",\n        \"#FF8D7662\" to \"Cement\",\n        \"#FF8D8974\" to \"Granite Green\",\n        \"#FF8D90A1\" to \"Manatee\",\n        \"#FF8DA8CC\" to \"Polo Blue\",\n        \"#FF8E0000\" to \"Red Berry\",\n        \"#FF8E4D1E\" to \"Rope\",\n        \"#FF8E6F70\" to \"Opium\",\n        \"#FF8E775E\" to \"Domino\",\n        \"#FF8E8190\" to \"Mamba\",\n        \"#FF8EABC1\" to \"Nepal\",\n        \"#FF8F021C\" to \"Pohutukawa\",\n        \"#FF8F3E33\" to \"El Salva\",\n        \"#FF8F4B0E\" to \"Korma\",\n        \"#FF8F8176\" to \"Squirrel\",\n        \"#FF8FD6B4\" to \"Vista Blue\",\n        \"#FF900020\" to \"Burgundy\",\n        \"#FF901E1E\" to \"Old Brick\",\n        \"#FF907874\" to \"Hemp\",\n        \"#FF907B71\" to \"Almond Frost\",\n        \"#FF908D39\" to \"Sycamore\",\n        \"#FF92000A\" to \"Sangria\",\n        \"#FF924321\" to \"Cumin\",\n        \"#FF926F5B\" to \"Beaver\",\n        \"#FF928573\" to \"Stonewall\",\n        \"#FF928590\" to \"Venus\",\n        \"#FF9370DB\" to \"Medium Purple\",\n        \"#FF93CCEA\" to \"Cornflower\",\n        \"#FF93DFB8\" to \"Algae Green\",\n        \"#FF944747\" to \"Copper Rust\",\n        \"#FF948771\" to \"Arrowtown\",\n        \"#FF950015\" to \"Scarlett\",\n        \"#FF956387\" to \"Strikemaster\",\n        \"#FF959396\" to \"Mountain Mist\",\n        \"#FF960018\" to \"Carmine\",\n        \"#FF964B00\" to \"Brown\",\n        \"#FF967059\" to \"Leather\",\n        \"#FF9678B6\" to \"Purple Mountain\",\n        \"#FF967BB6\" to \"Lavender Purple\",\n        \"#FF96A8A1\" to \"Pewter\",\n        \"#FF96BBAB\" to \"Summer Green\",\n        \"#FF97605D\" to \"Au Chico\",\n        \"#FF9771B5\" to \"Wisteria\",\n        \"#FF97CD2D\" to \"Atlantis\",\n        \"#FF983D61\" to \"Vin Rouge\",\n        \"#FF9874D3\" to \"Lilac Bush\",\n        \"#FF98777B\" to \"Bazaar\",\n        \"#FF98811B\" to \"Hacienda\",\n        \"#FF988D77\" to \"Pale Oyster\",\n        \"#FF98FF98\" to \"Mint Green\",\n        \"#FF990066\" to \"Fresh Eggplant\",\n        \"#FF991199\" to \"Violet Eggplant\",\n        \"#FF991613\" to \"Tamarillo\",\n        \"#FF991B07\" to \"Totem Pole\",\n        \"#FF996666\" to \"Copper Rose\",\n        \"#FF9966CC\" to \"Amethyst\",\n        \"#FF997A8D\" to \"Mountbatten Pink\",\n        \"#FF9999CC\" to \"Blue Bell\",\n        \"#FF9A3820\" to \"Prairie Sand\",\n        \"#FF9A6E61\" to \"Toast\",\n        \"#FF9A9577\" to \"Gurkha\",\n        \"#FF9AB973\" to \"Olivine\",\n        \"#FF9AC2B8\" to \"Shadow Green\",\n        \"#FF9B4703\" to \"Oregon\",\n        \"#FF9B9E8F\" to \"Lemon Grass\",\n        \"#FF9C3336\" to \"Stiletto\",\n        \"#FF9D5616\" to \"Hawaiian Tan\",\n        \"#FF9DACB7\" to \"Gull Gray\",\n        \"#FF9DC209\" to \"Pistachio\",\n        \"#FF9DE093\" to \"Granny Smith Apple\",\n        \"#FF9DE5FF\" to \"Anakiwa\",\n        \"#FF9E5302\" to \"Chelsea Gem\",\n        \"#FF9E5B40\" to \"Sepia Skin\",\n        \"#FF9EA587\" to \"Sage\",\n        \"#FF9EA91F\" to \"Citron\",\n        \"#FF9EB1CD\" to \"Rock Blue\",\n        \"#FF9EDEE0\" to \"Morning Glory\",\n        \"#FF9F381D\" to \"Cognac\",\n        \"#FF9F821C\" to \"Reef Gold\",\n        \"#FF9F9F9C\" to \"Star Dust\",\n        \"#FF9FA0B1\" to \"Santas Gray\",\n        \"#FF9FD7D3\" to \"Sinbad\",\n        \"#FF9FDD8C\" to \"Feijoa\",\n        \"#FFA02712\" to \"Tabasco\",\n        \"#FFA1750D\" to \"Buttered Rum\",\n        \"#FFA1ADB5\" to \"Hit Gray\",\n        \"#FFA1C50A\" to \"Citrus\",\n        \"#FFA1DAD7\" to \"Aqua Island\",\n        \"#FFA1E9DE\" to \"Water Leaf\",\n        \"#FFA2006D\" to \"Flirt\",\n        \"#FFA23B6C\" to \"Rouge\",\n        \"#FFA26645\" to \"Cape Palliser\",\n        \"#FFA2AAB3\" to \"Gray Chateau\",\n        \"#FFA2AEAB\" to \"Edward\",\n        \"#FFA3807B\" to \"Pharlap\",\n        \"#FFA397B4\" to \"Amethyst Smoke\",\n        \"#FFA3E3ED\" to \"Blizzard Blue\",\n        \"#FFA4A49D\" to \"Delta\",\n        \"#FFA4A6D3\" to \"Wistful\",\n        \"#FFA4AF6E\" to \"Green Smoke\",\n        \"#FFA50B5E\" to \"Jazzberry Jam\",\n        \"#FFA59B91\" to \"Zorba\",\n        \"#FFA5CB0C\" to \"Bahia\",\n        \"#FFA62F20\" to \"Roof Terracotta\",\n        \"#FFA65529\" to \"Paarl\",\n        \"#FFA68B5B\" to \"Barley Corn\",\n        \"#FFA69279\" to \"Donkey Brown\",\n        \"#FFA6A29A\" to \"Dawn\",\n        \"#FFA72525\" to \"Mexican Red\",\n        \"#FFA7882C\" to \"Luxor Gold\",\n        \"#FFA85307\" to \"Rich Gold\",\n        \"#FFA86515\" to \"Reno Sand\",\n        \"#FFA86B6B\" to \"Coral Tree\",\n        \"#FFA8989B\" to \"Dusty Gray\",\n        \"#FFA899E6\" to \"Dull Lavender\",\n        \"#FFA8A589\" to \"Tallow\",\n        \"#FFA8AE9C\" to \"Bud\",\n        \"#FFA8AF8E\" to \"Locust\",\n        \"#FFA8BD9F\" to \"Norway\",\n        \"#FFA8E3BD\" to \"Chinook\",\n        \"#FFA9A491\" to \"Gray Olive\",\n        \"#FFA9ACB6\" to \"Aluminium\",\n        \"#FFA9B2C3\" to \"Cadet Blue\",\n        \"#FFA9B497\" to \"Schist\",\n        \"#FFA9BDBF\" to \"Tower Gray\",\n        \"#FFA9BEF2\" to \"Perano\",\n        \"#FFA9C6C2\" to \"Opal\",\n        \"#FFAA375A\" to \"Night Shadz\",\n        \"#FFAA4203\" to \"Fire\",\n        \"#FFAA8B5B\" to \"Muesli\",\n        \"#FFAA8D6F\" to \"Sandal\",\n        \"#FFAAA5A9\" to \"Shady Lady\",\n        \"#FFAAA9CD\" to \"Logan\",\n        \"#FFAAABB7\" to \"Spun Pearl\",\n        \"#FFAAD6E6\" to \"Regent St Blue\",\n        \"#FFAAF0D1\" to \"Magic Mint\",\n        \"#FFAB0563\" to \"Lipstick\",\n        \"#FFAB3472\" to \"Royal Heath\",\n        \"#FFAB917A\" to \"Sandrift\",\n        \"#FFABA0D9\" to \"Cold Purple\",\n        \"#FFABA196\" to \"Bronco\",\n        \"#FFAC8A56\" to \"Limed Oak\",\n        \"#FFAC91CE\" to \"East Side\",\n        \"#FFAC9E22\" to \"Lemon Ginger\",\n        \"#FFACA494\" to \"Napa\",\n        \"#FFACA586\" to \"Hillary\",\n        \"#FFACA59F\" to \"Cloudy\",\n        \"#FFACACAC\" to \"Silver Chalice\",\n        \"#FFACB78E\" to \"Swamp Green\",\n        \"#FFACCBB1\" to \"Spring Rain\",\n        \"#FFACDD4D\" to \"Conifer\",\n        \"#FFACE1AF\" to \"Celadon\",\n        \"#FFAD781B\" to \"Mandalay\",\n        \"#FFADBED1\" to \"Casper\",\n        \"#FFADDFAD\" to \"Moss Green\",\n        \"#FFADE6C4\" to \"Padua\",\n        \"#FFADFF2F\" to \"Green Yellow\",\n        \"#FFAE4560\" to \"Hippie Pink\",\n        \"#FFAE6020\" to \"Desert\",\n        \"#FFAE809E\" to \"Bouquet\",\n        \"#FFAF4035\" to \"Medium Carmine\",\n        \"#FFAF4D43\" to \"Apple Blossom\",\n        \"#FFAF593E\" to \"Brown Rust\",\n        \"#FFAF8751\" to \"Driftwood\",\n        \"#FFAF8F2C\" to \"Alpine\",\n        \"#FFAF9F1C\" to \"Lucky\",\n        \"#FFAFA09E\" to \"Martini\",\n        \"#FFAFB1B8\" to \"Bombay\",\n        \"#FFAFBDD9\" to \"Pigeon Post\",\n        \"#FFB04C6A\" to \"Cadillac\",\n        \"#FFB05D54\" to \"Matrix\",\n        \"#FFB05E81\" to \"Tapestry\",\n        \"#FFB06608\" to \"Mai Tai\",\n        \"#FFB09A95\" to \"Del Rio\",\n        \"#FFB0E0E6\" to \"Powder Blue\",\n        \"#FFB0E313\" to \"Inch Worm\",\n        \"#FFB10000\" to \"Bright Red\",\n        \"#FFB14A0B\" to \"Vesuvius\",\n        \"#FFB1610B\" to \"Pumpkin Skin\",\n        \"#FFB16D52\" to \"Santa Fe\",\n        \"#FFB19461\" to \"Teak\",\n        \"#FFB1E2C1\" to \"Fringy Flower\",\n        \"#FFB1F4E7\" to \"Ice Cold\",\n        \"#FFB20931\" to \"Shiraz\",\n        \"#FFB2A1EA\" to \"Biloba Flower\",\n        \"#FFB32D29\" to \"Tall Poppy\",\n        \"#FFB35213\" to \"Fiery Orange\",\n        \"#FFB38007\" to \"Hot Toddy\",\n        \"#FFB3AF95\" to \"Taupe Gray\",\n        \"#FFB3C110\" to \"La Rioja\",\n        \"#FFB43332\" to \"Well Read\",\n        \"#FFB44668\" to \"Blush\",\n        \"#FFB4CFD3\" to \"Jungle Mist\",\n        \"#FFB57281\" to \"Turkish Rose\",\n        \"#FFB57EDC\" to \"Lavender\",\n        \"#FFB5A27F\" to \"Mongoose\",\n        \"#FFB5B35C\" to \"Olive Green\",\n        \"#FFB5D2CE\" to \"Jet Stream\",\n        \"#FFB5ECDF\" to \"Cruise\",\n        \"#FFB6316C\" to \"Hibiscus\",\n        \"#FFB69D98\" to \"Thatch\",\n        \"#FFB6B095\" to \"Heathered Gray\",\n        \"#FFB6BAA4\" to \"Eagle\",\n        \"#FFB6D1EA\" to \"Spindle\",\n        \"#FFB6D3BF\" to \"Gum Leaf\",\n        \"#FFB7410E\" to \"Rust\",\n        \"#FFB78E5C\" to \"Muddy Waters\",\n        \"#FFB7A214\" to \"Sahara\",\n        \"#FFB7A458\" to \"Husk\",\n        \"#FFB7B1B1\" to \"Nobel\",\n        \"#FFB7C3D0\" to \"Heather\",\n        \"#FFB7F0BE\" to \"Madang\",\n        \"#FFB81104\" to \"Milano Red\",\n        \"#FFB87333\" to \"Copper\",\n        \"#FFB8B56A\" to \"Gimblet\",\n        \"#FFB8C1B1\" to \"Green Spring\",\n        \"#FFB8C25D\" to \"Celery\",\n        \"#FFB8E0F9\" to \"Sail\",\n        \"#FFB94E48\" to \"Chestnut\",\n        \"#FFB95140\" to \"Crail\",\n        \"#FFB98D28\" to \"Marigold\",\n        \"#FFB9C46A\" to \"Wild Willow\",\n        \"#FFB9C8AC\" to \"Rainee\",\n        \"#FFBA0101\" to \"Guardsman Red\",\n        \"#FFBA450C\" to \"Rock Spray\",\n        \"#FFBA6F1E\" to \"Bourbon\",\n        \"#FFBA7F03\" to \"Pirate Gold\",\n        \"#FFBAB1A2\" to \"Nomad\",\n        \"#FFBAC7C9\" to \"Submarine\",\n        \"#FFBAEEF9\" to \"Charlotte\",\n        \"#FFBB3385\" to \"Medium Red Violet\",\n        \"#FFBB8983\" to \"Brandy Rose\",\n        \"#FFBBD009\" to \"Rio Grande\",\n        \"#FFBBD7C1\" to \"Surf\",\n        \"#FFBCC9C2\" to \"Powder Ash\",\n        \"#FFBD5E2E\" to \"Tuscany\",\n        \"#FFBD978E\" to \"Quicksand\",\n        \"#FFBDB1A8\" to \"Silk\",\n        \"#FFBDB2A1\" to \"Malta\",\n        \"#FFBDB3C7\" to \"Chatelle\",\n        \"#FFBDBBD7\" to \"Lavender Gray\",\n        \"#FFBDBDC6\" to \"French Gray\",\n        \"#FFBDC8B3\" to \"Clay Ash\",\n        \"#FFBDC9CE\" to \"Loblolly\",\n        \"#FFBDEDFD\" to \"French Pass\",\n        \"#FFBEA6C3\" to \"London Hue\",\n        \"#FFBEB5B7\" to \"Pink Swan\",\n        \"#FFBEDE0D\" to \"Fuego\",\n        \"#FFBF5500\" to \"Rose of Sharon\",\n        \"#FFBFB8B0\" to \"Tide\",\n        \"#FFBFBED8\" to \"Blue Haze\",\n        \"#FFBFC1C2\" to \"Silver Sand\",\n        \"#FFBFC921\" to \"Key Lime Pie\",\n        \"#FFBFDBE2\" to \"Ziggurat\",\n        \"#FFBFFF00\" to \"Lime\",\n        \"#FFC02B18\" to \"Thunderbird\",\n        \"#FFC04737\" to \"Mojo\",\n        \"#FFC08081\" to \"Old Rose\",\n        \"#FFC0C0C0\" to \"Silver\",\n        \"#FFC0D3B9\" to \"Pale Leaf\",\n        \"#FFC0D8B6\" to \"Pixie Green\",\n        \"#FFC1440E\" to \"Tia Maria\",\n        \"#FFC154C1\" to \"Fuchsia Pink\",\n        \"#FFC1A004\" to \"Buddha Gold\",\n        \"#FFC1B7A4\" to \"Bison Hide\",\n        \"#FFC1BAB0\" to \"Tea\",\n        \"#FFC1BECD\" to \"Gray Suit\",\n        \"#FFC1D7B0\" to \"Sprout\",\n        \"#FFC1F07C\" to \"Sulu\",\n        \"#FFC26B03\" to \"Indochine\",\n        \"#FFC2955D\" to \"Twine\",\n        \"#FFC2BDB6\" to \"Cotton Seed\",\n        \"#FFC2CAC4\" to \"Pumice\",\n        \"#FFC2E8E5\" to \"Jagged Ice\",\n        \"#FFC32148\" to \"Maroon Flush\",\n        \"#FFC3B091\" to \"Indian Khaki\",\n        \"#FFC3BFC1\" to \"Pale Slate\",\n        \"#FFC3C3BD\" to \"Gray Nickel\",\n        \"#FFC3CDE6\" to \"Periwinkle Gray\",\n        \"#FFC3D1D1\" to \"Tiara\",\n        \"#FFC3DDF9\" to \"Tropical Blue\",\n        \"#FFC41E3A\" to \"Cardinal\",\n        \"#FFC45655\" to \"Fuzzy Wuzzy Brown\",\n        \"#FFC45719\" to \"Orange Roughy\",\n        \"#FFC4C4BC\" to \"Mist Gray\",\n        \"#FFC4D0B0\" to \"Coriander\",\n        \"#FFC4F4EB\" to \"Mint Tulip\",\n        \"#FFC54B8C\" to \"Mulberry\",\n        \"#FFC59922\" to \"Nugget\",\n        \"#FFC5994B\" to \"Tussock\",\n        \"#FFC5DBCA\" to \"Sea Mist\",\n        \"#FFC5E17A\" to \"Yellow Green\",\n        \"#FFC62D42\" to \"Brick Red\",\n        \"#FFC6726B\" to \"Contessa\",\n        \"#FFC69191\" to \"Oriental Pink\",\n        \"#FFC6A84B\" to \"Roti\",\n        \"#FFC6C3B5\" to \"Ash\",\n        \"#FFC6C8BD\" to \"Kangaroo\",\n        \"#FFC6E610\" to \"Las Palmas\",\n        \"#FFC7031E\" to \"Monza\",\n        \"#FFC71585\" to \"Red Violet\",\n        \"#FFC7BCA2\" to \"Coral Reef\",\n        \"#FFC7C1FF\" to \"Melrose\",\n        \"#FFC7C4BF\" to \"Cloud\",\n        \"#FFC7C9D5\" to \"Ghost\",\n        \"#FFC7CD90\" to \"Pine Glade\",\n        \"#FFC7DDE5\" to \"Botticelli\",\n        \"#FFC88A65\" to \"Antique Brass\",\n        \"#FFC8A2C8\" to \"Lilac\",\n        \"#FFC8A528\" to \"Hokey Pokey\",\n        \"#FFC8AABF\" to \"Lily\",\n        \"#FFC8B568\" to \"Laser\",\n        \"#FFC8E3D7\" to \"Edgewater\",\n        \"#FFC96323\" to \"Piper\",\n        \"#FFC99415\" to \"Pizza\",\n        \"#FFC9A0DC\" to \"Light Wisteria\",\n        \"#FFC9B29B\" to \"Rodeo Dust\",\n        \"#FFC9B35B\" to \"Sundance\",\n        \"#FFC9B93B\" to \"Earls Green\",\n        \"#FFC9C0BB\" to \"Silver Rust\",\n        \"#FFC9D9D2\" to \"Conch\",\n        \"#FFC9FFA2\" to \"Reef\",\n        \"#FFC9FFE5\" to \"Aero Blue\",\n        \"#FFCA3435\" to \"Flush Mahogany\",\n        \"#FFCABB48\" to \"Turmeric\",\n        \"#FFCADCD4\" to \"Paris White\",\n        \"#FFCAE00D\" to \"Bitter Lemon\",\n        \"#FFCAE6DA\" to \"Skeptic\",\n        \"#FFCB8FA9\" to \"Viola\",\n        \"#FFCBCAB6\" to \"Foggy Gray\",\n        \"#FFCBD3B0\" to \"Green Mist\",\n        \"#FFCBDBD6\" to \"Nebula\",\n        \"#FFCC3333\" to \"Persian Red\",\n        \"#FFCC5501\" to \"Burnt Orange\",\n        \"#FFCC7722\" to \"Ochre\",\n        \"#FFCC8899\" to \"Puce\",\n        \"#FFCCCAA8\" to \"Thistle Green\",\n        \"#FFCCCCFF\" to \"Periwinkle\",\n        \"#FFCCFF00\" to \"Electric Lime\",\n        \"#FFCD5700\" to \"Tenn\",\n        \"#FFCD5C5C\" to \"Chestnut Rose\",\n        \"#FFCD8429\" to \"Brandy Punch\",\n        \"#FFCDF4FF\" to \"Onahau\",\n        \"#FFCEB98F\" to \"Sorrell Brown\",\n        \"#FFCEBABA\" to \"Cold Turkey\",\n        \"#FFCEC291\" to \"Yuma\",\n        \"#FFCEC7A7\" to \"Chino\",\n        \"#FFCFA39D\" to \"Eunry\",\n        \"#FFCFB53B\" to \"Old Gold\",\n        \"#FFCFDCCF\" to \"Tasman\",\n        \"#FFCFE5D2\" to \"Surf Crest\",\n        \"#FFCFF9F3\" to \"Humming Bird\",\n        \"#FFCFFAF4\" to \"Scandal\",\n        \"#FFD05F04\" to \"Red Stage\",\n        \"#FFD06DA1\" to \"Hopbush\",\n        \"#FFD07D12\" to \"Meteor\",\n        \"#FFD0BEF8\" to \"Perfume\",\n        \"#FFD0C0E5\" to \"Prelude\",\n        \"#FFD0F0C0\" to \"Tea Green\",\n        \"#FFD18F1B\" to \"Geebung\",\n        \"#FFD1BEA8\" to \"Vanilla\",\n        \"#FFD1C6B4\" to \"Soft Amber\",\n        \"#FFD1D2CA\" to \"Celeste\",\n        \"#FFD1D2DD\" to \"Mischka\",\n        \"#FFD1E231\" to \"Pear\",\n        \"#FFD2691E\" to \"Hot Cinnamon\",\n        \"#FFD27D46\" to \"Raw Sienna\",\n        \"#FFD29EAA\" to \"Careys Pink\",\n        \"#FFD2B48C\" to \"Tan\",\n        \"#FFD2DA97\" to \"Deco\",\n        \"#FFD2F6DE\" to \"Blue Romance\",\n        \"#FFD2F8B0\" to \"Gossip\",\n        \"#FFD3CBBA\" to \"Sisal\",\n        \"#FFD3CDC5\" to \"Swirl\",\n        \"#FFD47494\" to \"Charm\",\n        \"#FFD4B6AF\" to \"Clam Shell\",\n        \"#FFD4BF8D\" to \"Straw\",\n        \"#FFD4C4A8\" to \"Akaroa\",\n        \"#FFD4CD16\" to \"Bird Flower\",\n        \"#FFD4D7D9\" to \"Iron\",\n        \"#FFD4DFE2\" to \"Geyser\",\n        \"#FFD4E2FC\" to \"Hawkes Blue\",\n        \"#FFD54600\" to \"Grenadier\",\n        \"#FFD591A4\" to \"Can Can\",\n        \"#FFD59A6F\" to \"Whiskey\",\n        \"#FFD5D195\" to \"Winter Hazel\",\n        \"#FFD5F6E3\" to \"Granny Apple\",\n        \"#FFD69188\" to \"My Pink\",\n        \"#FFD6C562\" to \"Tacha\",\n        \"#FFD6CEF6\" to \"Moon Raker\",\n        \"#FFD6D6D1\" to \"Quill Gray\",\n        \"#FFD6FFDB\" to \"Snowy Mint\",\n        \"#FFD7837F\" to \"New York Pink\",\n        \"#FFD7C498\" to \"Pavlova\",\n        \"#FFD7D0FF\" to \"Fog\",\n        \"#FFD84437\" to \"Valencia\",\n        \"#FFD87C63\" to \"Japonica\",\n        \"#FFD8BFD8\" to \"Thistle\",\n        \"#FFD8C2D5\" to \"Maverick\",\n        \"#FFD8FCFA\" to \"Foam\",\n        \"#FFD94972\" to \"Cabaret\",\n        \"#FFD99376\" to \"Burning Sand\",\n        \"#FFD9B99B\" to \"Cameo\",\n        \"#FFD9D6CF\" to \"Timberwolf\",\n        \"#FFD9DCC1\" to \"Tana\",\n        \"#FFD9E4F5\" to \"Link Water\",\n        \"#FFD9F7FF\" to \"Mabel\",\n        \"#FFDA3287\" to \"Cerise\",\n        \"#FFDA5B38\" to \"Flame Pea\",\n        \"#FFDA6304\" to \"Bamboo\",\n        \"#FFDA6A41\" to \"Red Damask\",\n        \"#FFDA70D6\" to \"Orchid\",\n        \"#FFDA8A67\" to \"Copperfield\",\n        \"#FFDAA520\" to \"Golden Grass\",\n        \"#FFDAECD6\" to \"Zanah\",\n        \"#FFDAF4F0\" to \"Iceberg\",\n        \"#FFDAFAFF\" to \"Oyster Bay\",\n        \"#FFDB5079\" to \"Cranberry\",\n        \"#FFDB9690\" to \"Petite Orchid\",\n        \"#FFDB995E\" to \"Di Serria\",\n        \"#FFDBDBDB\" to \"Alto\",\n        \"#FFDBFFF8\" to \"Frosted Mint\",\n        \"#FFDC143C\" to \"Crimson\",\n        \"#FFDC4333\" to \"Punch\",\n        \"#FFDCB20C\" to \"Galliano\",\n        \"#FFDCB4BC\" to \"Blossom\",\n        \"#FFDCD747\" to \"Wattle\",\n        \"#FFDCD9D2\" to \"Westar\",\n        \"#FFDCDDCC\" to \"Moon Mist\",\n        \"#FFDCEDB4\" to \"Caper\",\n        \"#FFDCF0EA\" to \"Swans Down\",\n        \"#FFDDD6D5\" to \"Swiss Coffee\",\n        \"#FFDDF9F1\" to \"White Ice\",\n        \"#FFDE3163\" to \"Cerise Red\",\n        \"#FFDE6360\" to \"Roman\",\n        \"#FFDEA681\" to \"Tumbleweed\",\n        \"#FFDEBA13\" to \"Gold Tips\",\n        \"#FFDEC196\" to \"Brandy\",\n        \"#FFDECBC6\" to \"Wafer\",\n        \"#FFDED4A4\" to \"Sapling\",\n        \"#FFDED717\" to \"Barberry\",\n        \"#FFDEE5C0\" to \"Beryl Green\",\n        \"#FFDEF5FF\" to \"Pattens Blue\",\n        \"#FFDF73FF\" to \"Heliotrope\",\n        \"#FFDFBE6F\" to \"Apache\",\n        \"#FFDFCD6F\" to \"Chenin\",\n        \"#FFDFCFDB\" to \"Lola\",\n        \"#FFDFECDA\" to \"Willow Brook\",\n        \"#FFDFFF00\" to \"Chartreuse Yellow\",\n        \"#FFE0B0FF\" to \"Mauve\",\n        \"#FFE0B646\" to \"Anzac\",\n        \"#FFE0B974\" to \"Harvest Gold\",\n        \"#FFE0C095\" to \"Calico\",\n        \"#FFE0FFFF\" to \"Baby Blue\",\n        \"#FFE16865\" to \"Sunglo\",\n        \"#FFE1BC64\" to \"Equator\",\n        \"#FFE1C0C8\" to \"Pink Flare\",\n        \"#FFE1E6D6\" to \"Periglacial Blue\",\n        \"#FFE1EAD4\" to \"Kidnapper\",\n        \"#FFE1F6E8\" to \"Tara\",\n        \"#FFE25465\" to \"Mandy\",\n        \"#FFE2725B\" to \"Terracotta\",\n        \"#FFE28913\" to \"Golden Bell\",\n        \"#FFE292C0\" to \"Shocking\",\n        \"#FFE29418\" to \"Dixie\",\n        \"#FFE29CD2\" to \"Light Orchid\",\n        \"#FFE2D8ED\" to \"Snuff\",\n        \"#FFE2EBED\" to \"Mystic\",\n        \"#FFE2F3EC\" to \"Apple Green\",\n        \"#FFE30B5C\" to \"Razzmatazz\",\n        \"#FFE32636\" to \"Alizarin Crimson\",\n        \"#FFE34234\" to \"Cinnabar\",\n        \"#FFE3BEBE\" to \"Cavern Pink\",\n        \"#FFE3F5E1\" to \"Peppermint\",\n        \"#FFE3F988\" to \"Mindaro\",\n        \"#FFE47698\" to \"Deep Blush\",\n        \"#FFE49B0F\" to \"Gamboge\",\n        \"#FFE4C2D5\" to \"Melanie\",\n        \"#FFE4CFDE\" to \"Twilight\",\n        \"#FFE4D1C0\" to \"Bone\",\n        \"#FFE4D422\" to \"Sunflower\",\n        \"#FFE4D5B7\" to \"Grain Brown\",\n        \"#FFE4D69B\" to \"Zombie\",\n        \"#FFE4F6E7\" to \"Frostee\",\n        \"#FFE4FFD1\" to \"Snow Flurry\",\n        \"#FFE52B50\" to \"Amaranth\",\n        \"#FFE5841B\" to \"Zest\",\n        \"#FFE5CCC9\" to \"Dust Storm\",\n        \"#FFE5D7BD\" to \"Stark White\",\n        \"#FFE5D8AF\" to \"Hampton\",\n        \"#FFE5E0E1\" to \"Bon Jour\",\n        \"#FFE5E5E5\" to \"Mercury\",\n        \"#FFE5F9F6\" to \"Polar\",\n        \"#FFE64E03\" to \"Trinidad\",\n        \"#FFE6BE8A\" to \"Gold Sand\",\n        \"#FFE6BEA5\" to \"Cashmere\",\n        \"#FFE6D7B9\" to \"Double Spanish White\",\n        \"#FFE6E4D4\" to \"Satin Linen\",\n        \"#FFE6F2EA\" to \"Harp\",\n        \"#FFE6F8F3\" to \"Off Green\",\n        \"#FFE6FFE9\" to \"Hint of Green\",\n        \"#FFE6FFFF\" to \"Tranquil\",\n        \"#FFE77200\" to \"Mango Tango\",\n        \"#FFE7730A\" to \"Christine\",\n        \"#FFE79F8C\" to \"Tony's Pink\",\n        \"#FFE79FC4\" to \"Kobi\",\n        \"#FFE7BCB4\" to \"Rose Fog\",\n        \"#FFE7BF05\" to \"Corn\",\n        \"#FFE7CD8C\" to \"Putty\",\n        \"#FFE7ECE6\" to \"Gray Nurse\",\n        \"#FFE7F8FF\" to \"Lily White\",\n        \"#FFE7FEFF\" to \"Bubbles\",\n        \"#FFE89928\" to \"Fire Bush\",\n        \"#FFE8B9B3\" to \"Shilo\",\n        \"#FFE8E0D5\" to \"Pearl Bush\",\n        \"#FFE8EBE0\" to \"Green White\",\n        \"#FFE8F1D4\" to \"Chrome White\",\n        \"#FFE8F2EB\" to \"Gin\",\n        \"#FFE8F5F2\" to \"Aqua Squeeze\",\n        \"#FFE96E00\" to \"Clementine\",\n        \"#FFE97451\" to \"Burnt Sienna\",\n        \"#FFE97C07\" to \"Tahiti Gold\",\n        \"#FFE9CECD\" to \"Oyster Pink\",\n        \"#FFE9D75A\" to \"Confetti\",\n        \"#FFE9E3E3\" to \"Ebb\",\n        \"#FFE9F8ED\" to \"Ottoman\",\n        \"#FFE9FFFD\" to \"Clear Day\",\n        \"#FFEA88A8\" to \"Carissma\",\n        \"#FFEAAE69\" to \"Porsche\",\n        \"#FFEAB33B\" to \"Tulip Tree\",\n        \"#FFEAC674\" to \"Rob Roy\",\n        \"#FFEADAB8\" to \"Raffia\",\n        \"#FFEAE8D4\" to \"White Rock\",\n        \"#FFEAF6EE\" to \"Panache\",\n        \"#FFEAF6FF\" to \"Solitude\",\n        \"#FFEAF9F5\" to \"Aqua Spring\",\n        \"#FFEAFFFE\" to \"Dew\",\n        \"#FFEB9373\" to \"Apricot\",\n        \"#FFEBC2AF\" to \"Zinnwaldite\",\n        \"#FFECA927\" to \"Fuel Yellow\",\n        \"#FFECC54E\" to \"Ronchi\",\n        \"#FFECC7EE\" to \"French Lilac\",\n        \"#FFECCDB9\" to \"Just Right\",\n        \"#FFECE090\" to \"Wild Rice\",\n        \"#FFECEBBD\" to \"Fall Green\",\n        \"#FFECEBCE\" to \"Aths Special\",\n        \"#FFECF245\" to \"Starship\",\n        \"#FFED0A3F\" to \"Red Ribbon\",\n        \"#FFED7A1C\" to \"Tango\",\n        \"#FFED9121\" to \"Carrot Orange\",\n        \"#FFED989E\" to \"Sea Pink\",\n        \"#FFEDB381\" to \"Tacao\",\n        \"#FFEDC9AF\" to \"Desert Sand\",\n        \"#FFEDCDAB\" to \"Pancho\",\n        \"#FFEDDCB1\" to \"Chamois\",\n        \"#FFEDEA99\" to \"Primrose\",\n        \"#FFEDF5DD\" to \"Frost\",\n        \"#FFEDF5F5\" to \"Aqua Haze\",\n        \"#FFEDF6FF\" to \"Zumthor\",\n        \"#FFEDF9F1\" to \"Narvik\",\n        \"#FFEDFC84\" to \"Honeysuckle\",\n        \"#FFEE82EE\" to \"Lavender Magenta\",\n        \"#FFEEC1BE\" to \"Beauty Bush\",\n        \"#FFEED794\" to \"Chalky\",\n        \"#FFEED9C4\" to \"Almond\",\n        \"#FFEEDC82\" to \"Flax\",\n        \"#FFEEDEDA\" to \"Bizarre\",\n        \"#FFEEE3AD\" to \"Double Colonial White\",\n        \"#FFEEEEE8\" to \"Cararra\",\n        \"#FFEEEF78\" to \"Manz\",\n        \"#FFEEF0C8\" to \"Tahuna Sands\",\n        \"#FFEEF0F3\" to \"Athens Gray\",\n        \"#FFEEF3C3\" to \"Tusk\",\n        \"#FFEEF4DE\" to \"Loafer\",\n        \"#FFEEF6F7\" to \"Catskill White\",\n        \"#FFEEFDFF\" to \"Twilight Blue\",\n        \"#FFEEFF9A\" to \"Jonquil\",\n        \"#FFEEFFE2\" to \"Rice Flower\",\n        \"#FFEF863F\" to \"Jaffa\",\n        \"#FFEFEFEF\" to \"Gallery\",\n        \"#FFEFF2F3\" to \"Porcelain\",\n        \"#FFF091A9\" to \"Mauvelous\",\n        \"#FFF0D52D\" to \"Golden Dream\",\n        \"#FFF0DB7D\" to \"Golden Sand\",\n        \"#FFF0DC82\" to \"Buff\",\n        \"#FFF0E2EC\" to \"Prim\",\n        \"#FFF0E68C\" to \"Khaki\",\n        \"#FFF0EEFD\" to \"Selago\",\n        \"#FFF0EEFF\" to \"Titan White\",\n        \"#FFF0F8FF\" to \"Alice Blue\",\n        \"#FFF0FCEA\" to \"Feta\",\n        \"#FFF18200\" to \"Gold Drop\",\n        \"#FFF19BAB\" to \"Wewak\",\n        \"#FFF1E788\" to \"Sahara Sand\",\n        \"#FFF1E9D2\" to \"Parchment\",\n        \"#FFF1E9FF\" to \"Blue Chalk\",\n        \"#FFF1EEC1\" to \"Mint Julep\",\n        \"#FFF1F1F1\" to \"Seashell\",\n        \"#FFF1F7F2\" to \"Saltpan\",\n        \"#FFF1FFAD\" to \"Tidal\",\n        \"#FFF1FFC8\" to \"Chiffon\",\n        \"#FFF2552A\" to \"Flamingo\",\n        \"#FFF28500\" to \"Tangerine\",\n        \"#FFF2C3B2\" to \"Mandy's Pink\",\n        \"#FFF2F2F2\" to \"Concrete\",\n        \"#FFF2FAFA\" to \"Black Squeeze\",\n        \"#FFF34723\" to \"Pomegranate\",\n        \"#FFF3AD16\" to \"Buttercup\",\n        \"#FFF3D69D\" to \"New Orleans\",\n        \"#FFF3D9DF\" to \"Vanilla Ice\",\n        \"#FFF3E7BB\" to \"Sidecar\",\n        \"#FFF3E9E5\" to \"Dawn Pink\",\n        \"#FFF3EDCF\" to \"Wheatfield\",\n        \"#FFF3FB62\" to \"Canary\",\n        \"#FFF3FBD4\" to \"Orinoco\",\n        \"#FFF3FFD8\" to \"Carla\",\n        \"#FFF400A1\" to \"Hollywood Cerise\",\n        \"#FFF4A460\" to \"Sandy brown\",\n        \"#FFF4C430\" to \"Saffron\",\n        \"#FFF4D81C\" to \"Ripe Lemon\",\n        \"#FFF4EBD3\" to \"Janna\",\n        \"#FFF4F2EE\" to \"Pampas\",\n        \"#FFF4F4F4\" to \"Wild Sand\",\n        \"#FFF4F8FF\" to \"Zircon\",\n        \"#FFF57584\" to \"Froly\",\n        \"#FFF5C85C\" to \"Cream Can\",\n        \"#FFF5C999\" to \"Manhattan\",\n        \"#FFF5D5A0\" to \"Maize\",\n        \"#FFF5DEB3\" to \"Wheat\",\n        \"#FFF5E7A2\" to \"Sandwisp\",\n        \"#FFF5E7E2\" to \"Pot Pourri\",\n        \"#FFF5E9D3\" to \"Albescent White\",\n        \"#FFF5EDEF\" to \"Soft Peach\",\n        \"#FFF5F3E5\" to \"Ecru White\",\n        \"#FFF5F5DC\" to \"Beige\",\n        \"#FFF5FB3D\" to \"Golden Fizz\",\n        \"#FFF5FFBE\" to \"Australian Mint\",\n        \"#FFF64A8A\" to \"French Rose\",\n        \"#FFF653A6\" to \"Brilliant Rose\",\n        \"#FFF6A4C9\" to \"Illusion\",\n        \"#FFF6F0E6\" to \"Merino\",\n        \"#FFF6F7F7\" to \"Black Haze\",\n        \"#FFF6FFDC\" to \"Spring Sun\",\n        \"#FFF7468A\" to \"Violet Red\",\n        \"#FFF77703\" to \"Chilean Fire\",\n        \"#FFF77FBE\" to \"Persian Pink\",\n        \"#FFF7B668\" to \"Rajah\",\n        \"#FFF7C8DA\" to \"Azalea\",\n        \"#FFF7DBE6\" to \"We Peep\",\n        \"#FFF7F2E1\" to \"Quarter Spanish White\",\n        \"#FFF7F5FA\" to \"Whisper\",\n        \"#FFF7FAF7\" to \"Snow Drift\",\n        \"#FFF8B853\" to \"Casablanca\",\n        \"#FFF8C3DF\" to \"Chantilly\",\n        \"#FFF8D9E9\" to \"Cherub\",\n        \"#FFF8DB9D\" to \"Marzipan\",\n        \"#FFF8DD5C\" to \"Energy Yellow\",\n        \"#FFF8E4BF\" to \"Givry\",\n        \"#FFF8F0E8\" to \"White Linen\",\n        \"#FFF8F4FF\" to \"Magnolia\",\n        \"#FFF8F6F1\" to \"Spring Wood\",\n        \"#FFF8F7DC\" to \"Coconut Cream\",\n        \"#FFF8F7FC\" to \"White Lilac\",\n        \"#FFF8F8F7\" to \"Desert Storm\",\n        \"#FFF8F99C\" to \"Texas\",\n        \"#FFF8FACD\" to \"Corn Field\",\n        \"#FFF8FDD3\" to \"Mimosa\",\n        \"#FFF95A61\" to \"Carnation\",\n        \"#FFF9BF58\" to \"Saffron Mango\",\n        \"#FFF9E0ED\" to \"Carousel Pink\",\n        \"#FFF9E4BC\" to \"Dairy Cream\",\n        \"#FFF9E663\" to \"Portica\",\n        \"#FFF9EAF3\" to \"Amour\",\n        \"#FFF9F8E4\" to \"Rum Swizzle\",\n        \"#FFF9FF8B\" to \"Dolly\",\n        \"#FFF9FFF6\" to \"Sugar Cane\",\n        \"#FFFA7814\" to \"Ecstasy\",\n        \"#FFFA9D5A\" to \"Tan Hide\",\n        \"#FFFAD3A2\" to \"Corvette\",\n        \"#FFFADFAD\" to \"Peach Yellow\",\n        \"#FFFAE600\" to \"Turbo\",\n        \"#FFFAEAB9\" to \"Astra\",\n        \"#FFFAECCC\" to \"Champagne\",\n        \"#FFFAF0E6\" to \"Linen\",\n        \"#FFFAF3F0\" to \"Fantasy\",\n        \"#FFFAF7D6\" to \"Citrine White\",\n        \"#FFFAFAFA\" to \"Alabaster\",\n        \"#FFFAFDE4\" to \"Hint of Yellow\",\n        \"#FFFAFFA4\" to \"Milan\",\n        \"#FFFB607F\" to \"Brink Pink\",\n        \"#FFFB8989\" to \"Geraldine\",\n        \"#FFFBA0E3\" to \"Lavender Rose\",\n        \"#FFFBA129\" to \"Sea Buckthorn\",\n        \"#FFFBAC13\" to \"Sun\",\n        \"#FFFBAED2\" to \"Lavender Pink\",\n        \"#FFFBB2A3\" to \"Rose Bud\",\n        \"#FFFBBEDA\" to \"Cupid\",\n        \"#FFFBCCE7\" to \"Classic Rose\",\n        \"#FFFBCEB1\" to \"Apricot Peach\",\n        \"#FFFBE7B2\" to \"Banana Mania\",\n        \"#FFFBE870\" to \"Marigold Yellow\",\n        \"#FFFBE96C\" to \"Festival\",\n        \"#FFFBEA8C\" to \"Sweet Corn\",\n        \"#FFFBEC5D\" to \"Candy Corn\",\n        \"#FFFBF9F9\" to \"Hint of Red\",\n        \"#FFFBFFBA\" to \"Shalimar\",\n        \"#FFFC0FC0\" to \"Shocking Pink\",\n        \"#FFFC80A5\" to \"Tickle Me Pink\",\n        \"#FFFC9C1D\" to \"Tree Poppy\",\n        \"#FFFCC01E\" to \"Lightning Yellow\",\n        \"#FFFCD667\" to \"Goldenrod\",\n        \"#FFFCD917\" to \"Candlelight\",\n        \"#FFFCDA98\" to \"Cherokee\",\n        \"#FFFCF4D0\" to \"Double Pearl Lusta\",\n        \"#FFFCF4DC\" to \"Pearl Lusta\",\n        \"#FFFCF8F7\" to \"Vista White\",\n        \"#FFFCFBF3\" to \"Bianca\",\n        \"#FFFCFEDA\" to \"Moon Glow\",\n        \"#FFFCFFE7\" to \"China Ivory\",\n        \"#FFFCFFF9\" to \"Ceramic\",\n        \"#FFFD0E35\" to \"Torch Red\",\n        \"#FFFD5B78\" to \"Wild Watermelon\",\n        \"#FFFD7B33\" to \"Crusta\",\n        \"#FFFD7C07\" to \"Sorbus\",\n        \"#FFFD9FA2\" to \"Sweet Pink\",\n        \"#FFFDD5B1\" to \"Light Apricot\",\n        \"#FFFDD7E4\" to \"Pig Pink\",\n        \"#FFFDE1DC\" to \"Cinderella\",\n        \"#FFFDE295\" to \"Golden Glow\",\n        \"#FFFDE910\" to \"Lemon\",\n        \"#FFFDF5E6\" to \"Old Lace\",\n        \"#FFFDF6D3\" to \"Half Colonial White\",\n        \"#FFFDF7AD\" to \"Drover\",\n        \"#FFFDFEB8\" to \"Pale Prim\",\n        \"#FFFDFFD5\" to \"Cumulus\",\n        \"#FFFE28A2\" to \"Persian Rose\",\n        \"#FFFE4C40\" to \"Sunset Orange\",\n        \"#FFFE6F5E\" to \"Bittersweet\",\n        \"#FFFE9D04\" to \"California\",\n        \"#FFFEA904\" to \"Yellow Sea\",\n        \"#FFFEBAAD\" to \"Melon\",\n        \"#FFFED33C\" to \"Bright Sun\",\n        \"#FFFED85D\" to \"Dandelion\",\n        \"#FFFEDB8D\" to \"Salomie\",\n        \"#FFFEE5AC\" to \"Cape Honey\",\n        \"#FFFEEBF3\" to \"Remy\",\n        \"#FFFEEFCE\" to \"Oasis\",\n        \"#FFFEF0EC\" to \"Bridesmaid\",\n        \"#FFFEF2C7\" to \"Beeswax\",\n        \"#FFFEF3D8\" to \"Bleach White\",\n        \"#FFFEF4CC\" to \"Pipi\",\n        \"#FFFEF4DB\" to \"Half Spanish White\",\n        \"#FFFEF4F8\" to \"Wisp Pink\",\n        \"#FFFEF5F1\" to \"Provincial Pink\",\n        \"#FFFEF7DE\" to \"Half Dutch White\",\n        \"#FFFEF8E2\" to \"Solitaire\",\n        \"#FFFEF8FF\" to \"White Pointer\",\n        \"#FFFEF9E3\" to \"Off Yellow\",\n        \"#FFFEFCED\" to \"Orange White\",\n        \"#FFFF0000\" to \"Red\",\n        \"#FFFF007F\" to \"Rose\",\n        \"#FFFF00CC\" to \"Purple Pizzazz\",\n        \"#FFFF00FF\" to \"Magenta / Fuchsia\",\n        \"#FFFF2400\" to \"Scarlet\",\n        \"#FFFF3399\" to \"Wild Strawberry\",\n        \"#FFFF33CC\" to \"Razzle Dazzle Rose\",\n        \"#FFFF355E\" to \"Radical Red\",\n        \"#FFFF3F34\" to \"Red Orange\",\n        \"#FFFF4040\" to \"Coral Red\",\n        \"#FFFF4D00\" to \"Vermilion\",\n        \"#FFFF4F00\" to \"International Orange\",\n        \"#FFFF6037\" to \"Outrageous Orange\",\n        \"#FFFF6600\" to \"Blaze Orange\",\n        \"#FFFF66FF\" to \"Pink Flamingo\",\n        \"#FFFF681F\" to \"Orange\",\n        \"#FFFF69B4\" to \"Hot Pink\",\n        \"#FFFF6B53\" to \"Persimmon\",\n        \"#FFFF6FFF\" to \"Blush Pink\",\n        \"#FFFF7034\" to \"Burning Orange\",\n        \"#FFFF7518\" to \"Pumpkin\",\n        \"#FFFF7D07\" to \"Flamenco\",\n        \"#FFFF7F00\" to \"Flush Orange\",\n        \"#FFFF7F50\" to \"Coral\",\n        \"#FFFF8C69\" to \"Salmon\",\n        \"#FFFF9000\" to \"Pizazz\",\n        \"#FFFF910F\" to \"West Side\",\n        \"#FFFF91A4\" to \"Pink Salmon\",\n        \"#FFFF9933\" to \"Neon Carrot\",\n        \"#FFFF9966\" to \"Atomic Tangerine\",\n        \"#FFFF9980\" to \"Vivid Tangerine\",\n        \"#FFFF9E2C\" to \"Sunshade\",\n        \"#FFFFA000\" to \"Orange Peel\",\n        \"#FFFFA194\" to \"Mona Lisa\",\n        \"#FFFFA500\" to \"Web Orange\",\n        \"#FFFFA6C9\" to \"Carnation Pink\",\n        \"#FFFFAB81\" to \"Hit Pink\",\n        \"#FFFFAE42\" to \"Yellow Orange\",\n        \"#FFFFB0AC\" to \"Cornflower Lilac\",\n        \"#FFFFB1B3\" to \"Sundown\",\n        \"#FFFFB31F\" to \"My Sin\",\n        \"#FFFFB555\" to \"Texas Rose\",\n        \"#FFFFB7D5\" to \"Cotton Candy\",\n        \"#FFFFB97B\" to \"Macaroni and Cheese\",\n        \"#FFFFBA00\" to \"Selective Yellow\",\n        \"#FFFFBD5F\" to \"Koromiko\",\n        \"#FFFFBF00\" to \"Amber\",\n        \"#FFFFC0A8\" to \"Wax Flower\",\n        \"#FFFFC0CB\" to \"Pink\",\n        \"#FFFFC3C0\" to \"Your Pink\",\n        \"#FFFFC901\" to \"Supernova\",\n        \"#FFFFCBA4\" to \"Flesh\",\n        \"#FFFFCC33\" to \"Sunglow\",\n        \"#FFFFCC5C\" to \"Golden Tainoi\",\n        \"#FFFFCC99\" to \"Peach Orange\",\n        \"#FFFFCD8C\" to \"Chardonnay\",\n        \"#FFFFD1DC\" to \"Pastel Pink\",\n        \"#FFFFD2B7\" to \"Romantic\",\n        \"#FFFFD38C\" to \"Grandis\",\n        \"#FFFFD700\" to \"Gold\",\n        \"#FFFFD801\" to \"School bus Yellow\",\n        \"#FFFFD8D9\" to \"Cosmos\",\n        \"#FFFFDB58\" to \"Mustard\",\n        \"#FFFFDCD6\" to \"Peach Schnapps\",\n        \"#FFFFDDAF\" to \"Caramel\",\n        \"#FFFFDDCD\" to \"Tuft Bush\",\n        \"#FFFFDDCF\" to \"Watusi\",\n        \"#FFFFDDF4\" to \"Pink Lace\",\n        \"#FFFFDEAD\" to \"Navajo White\",\n        \"#FFFFDEB3\" to \"Frangipani\",\n        \"#FFFFE1DF\" to \"Pippin\",\n        \"#FFFFE1F2\" to \"Pale Rose\",\n        \"#FFFFE2C5\" to \"Negroni\",\n        \"#FFFFE5A0\" to \"Cream Brulee\",\n        \"#FFFFE5B4\" to \"Peach\",\n        \"#FFFFE6C7\" to \"Tequila\",\n        \"#FFFFE772\" to \"Kournikova\",\n        \"#FFFFEAC8\" to \"Sandy Beach\",\n        \"#FFFFEAD4\" to \"Karry\",\n        \"#FFFFEC13\" to \"Broom\",\n        \"#FFFFEDBC\" to \"Colonial White\",\n        \"#FFFFEED8\" to \"Derby\",\n        \"#FFFFEFA1\" to \"Vis Vis\",\n        \"#FFFFEFC1\" to \"Egg White\",\n        \"#FFFFEFD5\" to \"Papaya Whip\",\n        \"#FFFFEFEC\" to \"Fair Pink\",\n        \"#FFFFF0DB\" to \"Peach Cream\",\n        \"#FFFFF0F5\" to \"Lavender blush\",\n        \"#FFFFF14F\" to \"Gorse\",\n        \"#FFFFF1B5\" to \"Buttermilk\",\n        \"#FFFFF1D8\" to \"Pink Lady\",\n        \"#FFFFF1EE\" to \"Forget Me Not\",\n        \"#FFFFF1F9\" to \"Tutu\",\n        \"#FFFFF39D\" to \"Picasso\",\n        \"#FFFFF3F1\" to \"Chardon\",\n        \"#FFFFF46E\" to \"Paris Daisy\",\n        \"#FFFFF4CE\" to \"Barley White\",\n        \"#FFFFF4DD\" to \"Egg Sour\",\n        \"#FFFFF4E0\" to \"Sazerac\",\n        \"#FFFFF4E8\" to \"Serenade\",\n        \"#FFFFF4F3\" to \"Chablis\",\n        \"#FFFFF5EE\" to \"Seashell Peach\",\n        \"#FFFFF5F3\" to \"Sauvignon\",\n        \"#FFFFF6D4\" to \"Milk Punch\",\n        \"#FFFFF6DF\" to \"Varden\",\n        \"#FFFFF6F5\" to \"Rose White\",\n        \"#FFFFF8D1\" to \"Baja White\",\n        \"#FFFFF9E2\" to \"Gin Fizz\",\n        \"#FFFFF9E6\" to \"Early Dawn\",\n        \"#FFFFFACD\" to \"Lemon Chiffon\",\n        \"#FFFFFAF4\" to \"Bridal Heath\",\n        \"#FFFFFBDC\" to \"Scotch Mist\",\n        \"#FFFFFBF9\" to \"Soapstone\",\n        \"#FFFFFC99\" to \"Witch Haze\",\n        \"#FFFFFCEA\" to \"Buttery White\",\n        \"#FFFFFCEE\" to \"Island Spice\",\n        \"#FFFFFDD0\" to \"Cream\",\n        \"#FFFFFDE6\" to \"Chilean Heath\",\n        \"#FFFFFDE8\" to \"Travertine\",\n        \"#FFFFFDF3\" to \"Orchid White\",\n        \"#FFFFFDF4\" to \"Quarter Pearl Lusta\",\n        \"#FFFFFEE1\" to \"Half and Half\",\n        \"#FFFFFEEC\" to \"Apricot White\",\n        \"#FFFFFEF0\" to \"Rice Cake\",\n        \"#FFFFFEF6\" to \"Black White\",\n        \"#FFFFFEFD\" to \"Romance\",\n        \"#FFFFFF00\" to \"Yellow\",\n        \"#FFFFFF66\" to \"Laser Lemon\",\n        \"#FFFFFF99\" to \"Pale Canary\",\n        \"#FFFFFFB4\" to \"Portafino\",\n        \"#FFFFFFF0\" to \"Ivory\",\n        \"#FFFFFFFF\" to \"White\"\n    )\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/colorpick/model/ColorNameParser.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.colorpick.model\n\nimport androidx.compose.ui.graphics.Color\nimport cn.netdiscovery.monica.ui.controlpanel.colorpick.utils.hexToRGB\nimport cn.netdiscovery.monica.ui.controlpanel.colorpick.utils.toRGBArray\nimport kotlin.math.sqrt\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.colorpick.ColorNameParser\n * @author: Tony Shen\n * @date: 2024/6/13 17:04\n * @version: V1.0 <描述当前版本功能>\n */\nconst val Unspecified = \"?????\"\n\ninternal data class RGBData(val x: Int, val y: Int, val z: Int, val label: String)\n\nclass ColorNameParser internal constructor() {\n\n    private val rbgData: List<RGBData> by lazy {\n        colorNameMap.map { entry: Map.Entry<String, String> ->\n            val rgbArray = hexToRGB(entry.key)\n            val label = entry.value\n            RGBData(\n                x = rgbArray[0],\n                y = rgbArray[1],\n                z = rgbArray[2],\n                label = label\n            )\n        }\n    }\n\n    /**\n     * Parse name of [Color]\n     */\n    fun parseColorName(color: Color): String {\n        val rgbArray = color.toRGBArray()\n\n        val red: Int = rgbArray[0]\n        val green: Int = rgbArray[1]\n        val blue: Int = rgbArray[2]\n\n        var distance: Int=Int.MAX_VALUE\n\n        var colorId = -1\n\n        rbgData.forEachIndexed { index, rgbData ->\n            val currentDistance = sqrt(\n                (\n                        (rgbData.x - red) * (rgbData.x - red) +\n                                (rgbData.y - green) * (rgbData.y - green) +\n                                (rgbData.z - blue) * (rgbData.z - blue)\n                        ).toDouble()\n            ).toInt()\n\n            if (currentDistance < distance) {\n                distance = currentDistance\n                colorId = index\n            }\n        }\n\n        return   if (colorId >= 0) {\n            rbgData[colorId].label\n        } else Unspecified\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/colorpick/utils/ColorDetection.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.colorpick.utils\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.unit.IntRect\nimport org.jetbrains.skia.Bitmap\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.colorpick.ColorDetection\n * @author: Tony Shen\n * @date: 2024/6/13 22:03\n * @version: V1.0 <描述当前版本功能>\n */\n\nfun calculateColorInPixel(\n    offsetX: Float,\n    offsetY: Float,\n    startImageX: Float = 0f,\n    startImageY: Float = 0f,\n    rect: IntRect,\n    width: Float,\n    height: Float,\n    bitmap: Bitmap,\n): Color {\n\n    val bitmapWidth = bitmap.width\n    val bitmapHeight = bitmap.height\n\n    if (bitmapWidth == 0 || bitmapHeight == 0) return Color.Unspecified\n\n    // End positions, this might be less than Image dimensions if bitmap doesn't fit Image\n    val endImageX = width - startImageX\n    val endImageY = height - startImageY\n\n    val scaledX = scale(\n        start1 = startImageX,\n        end1 = endImageX,\n        pos = offsetX,\n        start2 = rect.left.toFloat(),\n        end2 = rect.right.toFloat()\n    ).toInt().coerceIn(0, bitmapWidth - 1)\n\n    val scaledY = scale(\n        start1 = startImageY,\n        end1 = endImageY,\n        pos = offsetY,\n        start2 = rect.top.toFloat(),\n        end2 = rect.bottom.toFloat()\n    ).toInt().coerceIn(0, bitmapHeight - 1)\n\n    val pixel: Int = bitmap.getColor(scaledX,scaledY)\n\n    val red = pixel shr 16 and 0xFF\n    val green = pixel shr 8 and 0xFF\n    val blue = pixel and 0xFF\n\n    return Color(red, green, blue)\n}\n\n/**\n * 线性插值\n */\nprivate fun lerp(start: Float, end: Float, amount: Float): Float {\n    return (1 - amount) * start + amount * end\n}\n\n/**\n * Scale x1 from start1..end1 range to start2..end2 range\n */\nprivate fun scale(start1: Float, end1: Float, pos: Float, start2: Float, end2: Float) =\n    lerp(start2, end2, calculateFraction(start1, end1, pos))\n\nprivate fun calculateFraction(start: Float, end: Float, pos: Float) =\n    (if (end - start == 0f) 0f else (pos - start) / (end - start)).coerceIn(0f, 1f)"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/colorpick/utils/ColorUtils.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.colorpick.utils\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.toArgb\nimport com.github.ajalt.colormath.model.RGB\nimport com.github.ajalt.colormath.model.RGBInt\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.colorpick.ColorUtils\n * @author: Tony Shen\n * @date: 2024/6/13 17:10\n * @version: V1.0 <描述当前版本功能>\n */\nfun hexToRGB(colorString: String): IntArray {\n\n    val completeColorString = if (colorString.first() == '#') colorString else \"#$colorString\"\n    val rgb = RGB(completeColorString)\n    return intArrayOf(rgb.redInt,rgb.greenInt, rgb.blueInt)\n}\n\nfun Color.toHex():String = RGBInt(this.toArgb().toUInt()).toSRGB().toHex()\n\nfun Color.toHSL():FloatArray = RGBInt(this.toArgb().toUInt()).toSRGB().toHSL().toArray()\n\nfun Color.toHSV():FloatArray = RGBInt(this.toArgb().toUInt()).toSRGB().toHSV().toArray()\n\nfun Color.toRGBArray(): IntArray {\n    val rgb = RGBInt(this.toArgb().toUInt()).toSRGB()\n    return intArrayOf(rgb.redInt,rgb.greenInt, rgb.blueInt)\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/colorpick/utils/RoundngUtils.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.colorpick.utils\n\nimport kotlin.math.roundToInt\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.colorpick.RoundngUtil\n * @author: Tony Shen\n * @date: 2024/6/13 20:37\n * @version: V1.0 <描述当前版本功能>\n */\n\n/**\n * Converts alpha, red, green or blue values from range of [0f-1f] to [0-255].\n */\nfun Float.fractionToRGBRange() = (this * 255.0f).toInt()\n\n/**\n * Converts alpha, red, green or blue values from range of [0f-1f] to [0-255] and returns\n * it as [String].\n */\nfun Float.fractionToRGBString() = this.fractionToRGBRange().toString()\n\n/**\n * Rounds this [Float] to another with 2 significant numbers\n * 0.1234 is rounded to 0.12\n * 0.127 is rounded to 0.13\n */\nfun Float.roundToTwoDigits() = (this * 100.0f).roundToInt() / 100.0f\n\n/**\n * Rounds this [Float] to closest int.\n */\nfun Float.round() = this.roundToInt()\n\n/**\n * Converts **HSV** or **HSL** colors that are in range of [0f-1f] to [0-100] range in [Integer]\n * with [Float.roundToInt]\n */\nfun Float.fractionToPercent() = (this * 100.0f).roundToInt()\n\n/**\n * Converts **HSV** or **HSL** colors that are in range of [0f-1f] to [0-100] range in [Integer]\n * with [Float.toInt]\n */\nfun Float.fractionToIntPercent() = (this * 100.0f).toInt()"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/colorpick/widget/ColorDisplay.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.colorpick.widget\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.Text\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.shadow\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport cn.netdiscovery.monica.ui.controlpanel.colorpick.model.ColorData\nimport cn.netdiscovery.monica.ui.controlpanel.colorpick.utils.toHSL\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.colorpick.ColorDisplay\n * @author: Tony Shen\n * @date: 2024/6/14 11:58\n * @version: V1.0 <描述当前版本功能>\n */\n@Composable\nfun ColorDisplay(\n    modifier: Modifier = Modifier,\n    colorData: ColorData\n) {\n    val color = colorData.color\n    val colorName = colorData.name\n\n    val lightness = color.toHSL()[2]\n    val textColor = if (lightness < .6f) Color.White else Color.Black\n\n    val hexText = colorData.hexText\n    Column(\n        modifier = modifier\n            .shadow(2.dp, RoundedCornerShape(10))\n            .width(170.dp)\n            .background(color = color)\n            .padding(start = 16.dp, end = 2.dp, top = 2.dp, bottom = 2.dp),\n    ) {\n\n        Row {\n            Column {\n                Text(text = hexText, fontSize = 20.sp, color = textColor)\n            }\n        }\n\n        Column {\n            Text(text = colorData.rgb, fontSize = 12.sp, color = textColor)\n            Text(text = colorData.hslString, fontSize = 12.sp, color = textColor)\n            Text(text = colorData.hsvString, fontSize = 12.sp, color = textColor)\n        }\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/colorpick/widget/ColorSelectionDrawing.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.colorpick.widget\n\nimport androidx.compose.foundation.Canvas\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.geometry.isSpecified\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.drawscope.Stroke\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport cn.netdiscovery.monica.ui.controlpanel.colorpick.defaultThumbnailSize\nimport kotlin.math.roundToInt\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.colorpick.ColorSelectionDrawing\n * @author: Tony Shen\n * @date: 2024/6/14 10:57\n * @version: V1.0 <描述当前版本功能>\n */\n@Composable\ninternal fun ColorSelectionDrawing(\n    modifier: Modifier,\n    thumbnailSize: Dp = defaultThumbnailSize,\n    offset: Offset,\n    thumbnailCenter: Offset,\n    color: Color\n) {\n    Canvas(modifier = modifier.fillMaxSize()) {\n\n        val canvasWidth = size.width\n        val canvasHeight = size.height\n\n        if (color != Color.Unspecified && offset.isSpecified && thumbnailCenter.isSpecified) {\n\n            // Get thumb size as parameter but limit max size to minimum of canvasWidth and Height\n            val imageThumbSize: Int =\n                thumbnailSize.toPx()\n                    .coerceAtMost(canvasWidth.coerceAtLeast(canvasHeight)).roundToInt()\n\n            val radius: Float = 8.dp.toPx()\n\n            // Draw touch position circle\n            drawCircle(\n                Color.Black,\n                radius = radius * 1.4f,\n                center = offset,\n                style = Stroke(radius * 0.4f)\n            )\n            drawCircle(\n                Color.White,\n                radius = radius * 1.0f,\n                center = offset,\n                style = Stroke(radius * 0.4f)\n            )\n\n            // Draw thumbnail center circle\n            drawCircle(\n                color = Color.Black,\n                radius = radius,\n                center = thumbnailCenter,\n                style = Stroke(radius * .5f)\n            )\n            drawCircle(\n                color = Color.White,\n                radius = radius,\n                center = thumbnailCenter,\n                style = Stroke(radius * .2f)\n            )\n\n            drawCircle(\n                color = color,\n                radius = imageThumbSize / 20f,\n                center = thumbnailCenter,\n            )\n        }\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/colorpick/widget/ImageColorDetector.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.colorpick.widget\n\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.geometry.isFinite\nimport androidx.compose.ui.geometry.isSpecified\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.ImageBitmap\nimport androidx.compose.ui.graphics.asSkiaBitmap\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.DpSize\nimport cn.netdiscovery.monica.ui.controlpanel.colorpick.OnColorChange\nimport cn.netdiscovery.monica.ui.controlpanel.colorpick.defaultThumbnailSize\nimport cn.netdiscovery.monica.ui.controlpanel.colorpick.model.ColorData\nimport cn.netdiscovery.monica.ui.controlpanel.colorpick.model.ColorNameParser\nimport cn.netdiscovery.monica.ui.controlpanel.colorpick.utils.calculateColorInPixel\nimport cn.netdiscovery.monica.ui.widget.image.ImageWithThumbnail\nimport cn.netdiscovery.monica.ui.widget.image.rememberThumbnailState\nimport com.safframework.kotlin.coroutines.IO\nimport kotlinx.coroutines.flow.distinctUntilChanged\nimport kotlinx.coroutines.flow.flowOn\nimport kotlinx.coroutines.flow.mapLatest\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.colorpick.ImageColorDetector\n * @author: Tony Shen\n * @date: 2024/6/13 20:13\n * @version: V1.0 <描述当前版本功能>\n */\n@Composable\nfun ImageColorDetector(\n    modifier: Modifier = Modifier,\n    imageBitmap: ImageBitmap,\n    contentScale: ContentScale = ContentScale.FillBounds,\n    alignment: Alignment = Alignment.Center,\n    colorNameParser: ColorNameParser,\n    thumbnailSize: Dp = defaultThumbnailSize,\n    thumbnailZoom: Int = 200,\n    onColorChange: OnColorChange\n) {\n\n    var offset by remember(imageBitmap, contentScale) {\n        mutableStateOf(Offset.Unspecified)\n    }\n\n    var center by remember(imageBitmap, contentScale) {\n        mutableStateOf(Offset.Unspecified)\n    }\n\n    var color by remember { mutableStateOf(Color.Unspecified) }\n\n    LaunchedEffect(key1 = colorNameParser) {\n\n        snapshotFlow { color }\n            .distinctUntilChanged()\n            .mapLatest { color: Color ->\n                colorNameParser.parseColorName(color)\n            }\n            .flowOn(IO)\n            .collect { name: String ->\n                onColorChange(ColorData(color, name))\n            }\n    }\n\n    ImageWithThumbnail(\n        imageBitmap = imageBitmap,\n        modifier = modifier,\n        contentDescription = \"Image Color Detector\",\n        contentScale = contentScale,\n        alignment = alignment,\n        thumbnailState = rememberThumbnailState(\n            size = DpSize(thumbnailSize, thumbnailSize),\n            thumbnailZoom = thumbnailZoom,\n        ),\n        onThumbnailCenterChange = {\n            center = it\n        },\n        onDown = {\n            offset = it\n        },\n        onMove = {\n            offset = it\n        }\n    ) {\n\n        val density = LocalDensity.current.density\n\n        if (offset.isSpecified && offset.isFinite) {\n            color = calculateColorInPixel(\n                offsetX = offset.x,\n                offsetY = offset.y,\n                startImageX = 0f,\n                startImageY = 0f,\n                rect = rect,\n                width = imageWidth.value * density,\n                height = imageHeight.value * density,\n                bitmap = imageBitmap.asSkiaBitmap()\n            )\n        }\n\n        ColorSelectionDrawing(\n            modifier = Modifier.size(imageWidth, imageHeight),\n            offset = offset,\n            thumbnailCenter = center,\n            color = color\n        )\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/compression/CompressionActions.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.compression\n\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.material.*\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.dp\nimport cn.netdiscovery.monica.state.ApplicationState\nimport cn.netdiscovery.monica.utils.ImageCompressionUtils\nimport java.io.File\nimport javax.swing.JFileChooser\nimport javax.swing.JFrame\n\n@Composable\nfun CompressionActionButtons(\n    viewModel: CompressionViewModel,\n    state: ApplicationState,\n    onShowToast: (String) -> Unit,\n    i18nState: cn.netdiscovery.monica.ui.i18n.I18nState\n) {\n    Row(\n        modifier = Modifier.fillMaxWidth(),\n        horizontalArrangement = Arrangement.spacedBy(8.dp)\n    ) {\n        Button(\n            onClick = {\n                viewModel.applyCompressedImage(state)\n                onShowToast(i18nState.getString(\"applied_to_editor\"))\n            },\n            modifier = Modifier\n                .weight(1f)\n                .height(40.dp),\n            colors = ButtonDefaults.buttonColors(\n                backgroundColor = MaterialTheme.colors.primary\n            )\n        ) {\n            Text(\n                i18nState.getString(\"apply_to_editor\"),\n                color = Color.White,\n                fontWeight = FontWeight.Bold\n            )\n        }\n\n        Button(\n            onClick = {\n                // 撤销：优先撤销“应用到编辑器”（如果有），同时重置参数并清理压缩结果，回到原图预览\n                val ok = viewModel.undoApplied(state)\n                viewModel.resetAll()\n                onShowToast(\n                    if (ok) i18nState.getString(\"undo_and_reset_success\")\n                    else i18nState.getString(\"reset_done\")\n                )\n            },\n            modifier = Modifier\n                .width(92.dp)\n                .height(40.dp),\n            colors = ButtonDefaults.buttonColors(\n                backgroundColor = MaterialTheme.colors.primary.copy(alpha = 0.7f)\n            )\n        ) {\n            Text(\n                i18nState.getString(\"undo\"),\n                color = Color.White\n            )\n        }\n        \n        Button(\n            onClick = {\n                val fileChooser = JFileChooser()\n                fileChooser.fileSelectionMode = JFileChooser.FILES_ONLY\n                fileChooser.selectedFile = File(\"compressed.${viewModel.selectedAlgorithm.format}\")\n                val result = fileChooser.showSaveDialog(JFrame())\n                if (result == JFileChooser.APPROVE_OPTION) {\n                    val outputFile = fileChooser.selectedFile\n                    val saveResult = viewModel.saveLastCompressedToFile(outputFile)\n                    if (saveResult != null) {\n                        onShowToast(i18nState.getString(\"save_success\").format(saveResult.outputFile.absolutePath))\n                    } else {\n                        onShowToast(i18nState.getString(\"save_failed\"))\n                    }\n                }\n            },\n            modifier = Modifier\n                .weight(1f)\n                .height(40.dp),\n            colors = ButtonDefaults.buttonColors(\n                backgroundColor = MaterialTheme.colors.primary.copy(alpha = 0.7f)\n            )\n        ) {\n            Text(\n                i18nState.getString(\"save\"),\n                color = Color.White\n            )\n        }\n    }\n}\n\n\n"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/compression/CompressionAlgorithmDropdown.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.compression\n\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.material.*\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport cn.netdiscovery.monica.utils.CompressionAlgorithm\n\n@Composable\nfun CompressionAlgorithmDropdown(\n    viewModel: CompressionViewModel,\n    i18nState: cn.netdiscovery.monica.ui.i18n.I18nState\n) {\n    var expanded by remember { mutableStateOf(false) }\n    \n    Column {\n        Text(\n            text = i18nState.getString(\"compression_algorithm\"),\n            style = MaterialTheme.typography.subtitle2,\n            fontWeight = FontWeight.Bold,\n            modifier = Modifier.padding(bottom = 8.dp)\n        )\n        \n        Box {\n            OutlinedButton(\n                onClick = { expanded = true },\n                modifier = Modifier.fillMaxWidth(),\n                colors = ButtonDefaults.outlinedButtonColors(\n                    backgroundColor = MaterialTheme.colors.surface\n                )\n            ) {\n                Row(\n                    modifier = Modifier.fillMaxWidth(),\n                    horizontalArrangement = Arrangement.SpaceBetween,\n                    verticalAlignment = Alignment.CenterVertically\n                ) {\n                    Text(\n                        text = viewModel.selectedAlgorithm.displayName,\n                        color = MaterialTheme.colors.onSurface\n                    )\n                    Text(\n                        text = \"▼\",\n                        color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f),\n                        fontSize = 12.sp\n                    )\n                }\n            }\n            \n            DropdownMenu(\n                expanded = expanded,\n                onDismissRequest = { expanded = false },\n                modifier = Modifier.fillMaxWidth()\n            ) {\n                CompressionAlgorithm.entries.forEach { algorithm ->\n                    DropdownMenuItem(\n                        onClick = {\n                            viewModel.selectedAlgorithm = algorithm\n                            expanded = false\n                        }\n                    ) {\n                        Text(algorithm.displayName)\n                    }\n                }\n            }\n        }\n    }\n}\n\n\n\n\n\n"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/compression/CompressionInputSection.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.compression\n\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.material.*\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.dp\nimport cn.netdiscovery.monica.state.ApplicationState\nimport java.io.File\nimport javax.swing.JFileChooser\nimport javax.swing.JFrame\n\n@Composable\nfun CompressionInputSection(\n    compressionMode: CompressionMode,\n    onModeChange: (CompressionMode) -> Unit,\n    selectedOutputDir: File?,\n    onOutputDirSelected: (File) -> Unit,\n    viewModel: CompressionViewModel,\n    state: ApplicationState,\n    i18nState: cn.netdiscovery.monica.ui.i18n.I18nState,\n    onShowToast: (String) -> Unit\n) {\n\n    Column(\n        verticalArrangement = Arrangement.spacedBy(12.dp)\n    ) {\n        Text(\n            text = i18nState.getString(\"input_selection\"),\n            style = MaterialTheme.typography.subtitle2,\n            fontWeight = FontWeight.Bold\n        )\n        \n        Row(\n            modifier = Modifier.fillMaxWidth(),\n            horizontalArrangement = Arrangement.spacedBy(8.dp)\n        ) {\n            Button(\n                onClick = { onModeChange(CompressionMode.SINGLE) },\n                modifier = Modifier\n                    .weight(1f)\n                    .height(40.dp),\n                colors = ButtonDefaults.buttonColors(\n                    backgroundColor = if (compressionMode == CompressionMode.SINGLE)\n                        MaterialTheme.colors.primary\n                    else\n                        MaterialTheme.colors.surface\n                )\n            ) {\n                Icon(\n                    painter = painterResource(\"images/controlpanel/compress.png\"),\n                    contentDescription = null,\n                    modifier = Modifier.size(20.dp)\n                )\n                Spacer(modifier = Modifier.width(8.dp))\n                Text(i18nState.getString(\"single_image\"))\n            }\n            \n            Button(\n                onClick = { onModeChange(CompressionMode.BATCH) },\n                modifier = Modifier\n                    .weight(1f)\n                    .height(40.dp),\n                colors = ButtonDefaults.buttonColors(\n                    backgroundColor = if (compressionMode == CompressionMode.BATCH)\n                        MaterialTheme.colors.primary\n                    else\n                        MaterialTheme.colors.surface\n                )\n            ) {\n                Icon(\n                    painter = painterResource(\"images/controlpanel/compress.png\"),\n                    contentDescription = null,\n                    modifier = Modifier.size(20.dp)\n                )\n                Spacer(modifier = Modifier.width(8.dp))\n                Text(i18nState.getString(\"batch_folder\"))\n            }\n        }\n        \n        if (compressionMode == CompressionMode.SINGLE) {\n            // 单张图模式：只显示\"开始压缩\"按钮\n            Button(\n                onClick = {\n                    if (viewModel.selectedImage == null) {\n                        onShowToast(i18nState.getString(\"please_select_image_in_preview\"))\n                        return@Button\n                    }\n                    \n                    // 开始压缩（不选择保存位置，压缩后显示在右侧预览区）\n                    viewModel.compressSingleImageToPreview(state.scope) { i18nState.getString(it) }\n                },\n                enabled = !viewModel.isCompressing && viewModel.selectedImage != null,\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .height(40.dp),\n                colors = ButtonDefaults.buttonColors(\n                    backgroundColor = MaterialTheme.colors.primary\n                )\n            ) {\n                Text(\n                    i18nState.getString(\"start_compression\"),\n                    color = Color.White,\n                    fontWeight = FontWeight.Bold\n                )\n            }\n\n            // Reset：重置参数 + 清掉压缩结果（恢复到原图预览）\n            OutlinedButton(\n                onClick = { viewModel.resetAll() },\n                enabled = !viewModel.isCompressing && (!viewModel.isAtDefaultParams() || viewModel.showResult),\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .height(40.dp)\n            ) {\n                Text(i18nState.getString(\"reset\"))\n            }\n        } else {\n            Button(\n                onClick = {\n                    val fileChooser = JFileChooser()\n                    fileChooser.fileSelectionMode = JFileChooser.DIRECTORIES_ONLY\n                    val result = fileChooser.showOpenDialog(JFrame())\n                    if (result == JFileChooser.APPROVE_OPTION) {\n                        onOutputDirSelected(fileChooser.selectedFile)\n                    }\n                },\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .height(40.dp),\n                colors = ButtonDefaults.buttonColors(\n                    backgroundColor = MaterialTheme.colors.primary.copy(alpha = 0.7f)\n                )\n            ) {\n                Text(\n                    if (selectedOutputDir == null)\n                        i18nState.getString(\"select_input_folder\")\n                    else\n                        \"${i18nState.getString(\"selected\")}: ${selectedOutputDir!!.name}\",\n                    color = Color.White\n                )\n            }\n            \n            if (selectedOutputDir != null) {\n                Button(\n                    onClick = {\n                        val fileChooser = JFileChooser()\n                        fileChooser.fileSelectionMode = JFileChooser.DIRECTORIES_ONLY\n                        fileChooser.dialogTitle = i18nState.getString(\"select_output_folder\")\n                        val result = fileChooser.showOpenDialog(JFrame())\n                        if (result == JFileChooser.APPROVE_OPTION) {\n                            val outputDir = fileChooser.selectedFile\n                            \n                            // 检查输出文件夹中是否已有文件\n                            val existingFiles = outputDir.listFiles()?.filter { it.isFile }?.size ?: 0\n                            if (existingFiles > 0) {\n                                val confirmResult = javax.swing.JOptionPane.showConfirmDialog(\n                                    null,\n                                    i18nState.getString(\"output_folder_has_files\").format(existingFiles),\n                                    i18nState.getString(\"batch_compression_warning\"),\n                                    javax.swing.JOptionPane.YES_NO_OPTION,\n                                    javax.swing.JOptionPane.WARNING_MESSAGE\n                                )\n                                if (confirmResult != javax.swing.JOptionPane.YES_OPTION) {\n                                    return@Button\n                                }\n                            }\n                            \n                            viewModel.compressBatch(selectedOutputDir!!, outputDir, state.scope) { i18nState.getString(it) }\n                        }\n                    },\n                    enabled = !viewModel.isCompressing,\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .height(40.dp),\n                    colors = ButtonDefaults.buttonColors(\n                        backgroundColor = MaterialTheme.colors.primary\n                    )\n                ) {\n                    Text(\n                        i18nState.getString(\"start_batch_compression\"),\n                        color = Color.White,\n                        fontWeight = FontWeight.Bold\n                    )\n                }\n            }\n        }\n    }\n}\n\n"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/compression/CompressionPreview.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.compression\n\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.*\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Close\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.toPainter\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.dp\nimport kotlin.math.abs\nimport cn.netdiscovery.monica.state.ApplicationState\nimport cn.netdiscovery.monica.utils.*\nimport cn.netdiscovery.monica.utils.chooseImage\nimport java.io.File\nimport javax.imageio.ImageIO\n\n@Composable\nfun CompressionRightPanel(\n    modifier: Modifier = Modifier,\n    state: ApplicationState,\n    viewModel: CompressionViewModel,\n    compressionMode: CompressionMode,\n    onShowToast: (String) -> Unit,\n    i18nState: cn.netdiscovery.monica.ui.i18n.I18nState\n) {\n    Card(\n        modifier = modifier,\n        shape = RoundedCornerShape(12.dp),\n        elevation = 4.dp,\n        backgroundColor = MaterialTheme.colors.surface\n    ) {\n        Column(\n            modifier = Modifier.fillMaxSize()\n        ) {\n            // 图片预览区域\n            Box(\n                modifier = Modifier\n                    .weight(1f)\n                    .fillMaxWidth()\n                    .background(MaterialTheme.colors.background)\n                    .padding(16.dp),\n                contentAlignment = Alignment.Center\n            ) {\n                CompressionPreviewArea(\n                    state = state,\n                    viewModel = viewModel,\n                    compressionMode = compressionMode,\n                    onShowToast = onShowToast,\n                    i18nState = i18nState\n                )\n            }\n            \n            // WebP 降级警告\n            if (viewModel.webpFallbackWarning != null) {\n                Card(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .padding(horizontal = 16.dp, vertical = 8.dp),\n                    backgroundColor = MaterialTheme.colors.error.copy(alpha = 0.1f),\n                    shape = RoundedCornerShape(8.dp)\n                ) {\n                    Row(\n                        modifier = Modifier\n                            .fillMaxWidth()\n                            .padding(12.dp),\n                        verticalAlignment = Alignment.CenterVertically,\n                        horizontalArrangement = Arrangement.spacedBy(8.dp)\n                    ) {\n                        Icon(\n                            painter = painterResource(\"images/controlpanel/compress.png\"),\n                            contentDescription = null,\n                            modifier = Modifier.size(20.dp),\n                            tint = MaterialTheme.colors.error\n                        )\n                        Text(\n                            text = viewModel.webpFallbackWarning!!,\n                            style = MaterialTheme.typography.body2,\n                            color = MaterialTheme.colors.error\n                        )\n                    }\n                }\n            }\n\n            // 压缩后文件变大提示\n            if (viewModel.sizeChangeWarning != null) {\n                Card(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .padding(horizontal = 16.dp, vertical = 8.dp),\n                    backgroundColor = MaterialTheme.colors.error.copy(alpha = 0.08f),\n                    shape = RoundedCornerShape(8.dp)\n                ) {\n                    Row(\n                        modifier = Modifier\n                            .fillMaxWidth()\n                            .padding(12.dp),\n                        verticalAlignment = Alignment.CenterVertically,\n                        horizontalArrangement = Arrangement.spacedBy(8.dp)\n                    ) {\n                        Icon(\n                            painter = painterResource(\"images/controlpanel/compress.png\"),\n                            contentDescription = null,\n                            modifier = Modifier.size(20.dp),\n                            tint = MaterialTheme.colors.error\n                        )\n                        Text(\n                            text = viewModel.sizeChangeWarning!!,\n                            style = MaterialTheme.typography.body2,\n                            color = MaterialTheme.colors.error\n                        )\n                    }\n                }\n            }\n            \n            // 文件大小信息和操作按钮（仅在单张图模式下显示操作按钮）\n            if (viewModel.showResult) {\n                CompressionResultInfo(\n                    viewModel = viewModel,\n                    state = state,\n                    compressionMode = compressionMode,\n                    onShowToast = onShowToast,\n                    i18nState = i18nState\n                )\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun CompressionPreviewArea(\n    state: ApplicationState,\n    viewModel: CompressionViewModel,\n    compressionMode: CompressionMode,\n    onShowToast: (String) -> Unit,\n    i18nState: cn.netdiscovery.monica.ui.i18n.I18nState\n) {\n    // 批量模式：右侧仅作为信息展示区，避免“无意义预览”\n    if (compressionMode == CompressionMode.BATCH) {\n        Column(\n            horizontalAlignment = Alignment.CenterHorizontally,\n            verticalArrangement = Arrangement.spacedBy(8.dp)\n        ) {\n            Icon(\n                painter = painterResource(\"images/controlpanel/compress.png\"),\n                contentDescription = null,\n                modifier = Modifier.size(48.dp),\n                tint = MaterialTheme.colors.onSurface.copy(alpha = 0.35f)\n            )\n            Text(\n                text = i18nState.getString(\"batch_mode_no_preview\"),\n                color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f),\n                style = MaterialTheme.typography.body2\n            )\n        }\n        return\n    }\n\n    val original = viewModel.selectedImage\n    val compressed = viewModel.compressedImage\n\n    if (original == null) {\n        if (compressionMode == CompressionMode.SINGLE) {\n            Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(16.dp)) {\n                Icon(\n                    painter = painterResource(\"images/controlpanel/compress.png\"),\n                    contentDescription = null,\n                    modifier = Modifier.size(64.dp),\n                    tint = MaterialTheme.colors.onSurface.copy(alpha = 0.4f)\n                )\n                Text(\n                    text = i18nState.getString(\"please_select_image_to_compress\"),\n                    color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f),\n                    style = MaterialTheme.typography.body1\n                )\n                Button(\n                    onClick = {\n                        chooseImage(state) { selectedFile ->\n                            try {\n                                val fileSize = selectedFile.length()\n                                val image = ImageIO.read(selectedFile)\n                                if (image != null) {\n                                    viewModel.selectedImage = image\n                                    viewModel.selectedImageFile = selectedFile\n                                    viewModel.selectedImageFileSize = fileSize\n                                } else {\n                                    onShowToast(i18nState.getString(\"cannot_read_image_file\"))\n                                }\n                            } catch (e: Exception) {\n                                onShowToast(i18nState.getString(\"load_image_failed\").format(e.message ?: \"\"))\n                            }\n                        }\n                    },\n                    enabled = !viewModel.isCompressing,\n                    colors = ButtonDefaults.buttonColors(backgroundColor = MaterialTheme.colors.primary)\n                ) {\n                    Text(i18nState.getString(\"select_image\"), color = Color.White, fontWeight = FontWeight.Bold)\n                }\n            }\n        } else {\n            Text(\n                text = i18nState.getString(\"please_select_or_load_image\"),\n                color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)\n            )\n        }\n        return\n    }\n\n    // 单图模式：右侧优先展示\"压缩后的预览\"；Reset/清理结果后会回到原图展示\n    val displayImage = if (viewModel.showResult && compressed != null) compressed else original\n\n    Box(modifier = Modifier.fillMaxSize()) {\n        Image(\n            painter = displayImage.toPainter(),\n            contentDescription = null,\n            modifier = Modifier.fillMaxSize(),\n            contentScale = ContentScale.Fit\n        )\n        \n        // 清除图像按钮（悬浮在右上角）\n        IconButton(\n            onClick = {\n                viewModel.clearSelectedImage()\n                onShowToast(i18nState.getString(\"image_cleared\"))\n            },\n            modifier = Modifier\n                .align(Alignment.TopEnd)\n                .padding(8.dp)\n                .background(\n                    MaterialTheme.colors.surface.copy(alpha = 0.9f),\n                    RoundedCornerShape(8.dp)\n                ),\n            enabled = !viewModel.isCompressing\n        ) {\n            Icon(\n                imageVector = Icons.Default.Close,\n                contentDescription = i18nState.getString(\"clear_image\"),\n                tint = MaterialTheme.colors.error,\n                modifier = Modifier.size(24.dp)\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun CompressionResultInfo(\n    viewModel: CompressionViewModel,\n    state: ApplicationState,\n    compressionMode: CompressionMode,\n    onShowToast: (String) -> Unit,\n    i18nState: cn.netdiscovery.monica.ui.i18n.I18nState\n) {\n    Column(\n        modifier = Modifier\n            .fillMaxWidth()\n            .padding(16.dp),\n        verticalArrangement = Arrangement.spacedBy(12.dp)\n    ) {\n        Row(\n            modifier = Modifier.fillMaxWidth(),\n            horizontalArrangement = Arrangement.SpaceBetween,\n            verticalAlignment = Alignment.CenterVertically\n        ) {\n            Text(\n                text = \"${i18nState.getString(\"original\")}: ${ImageCompressionUtils.formatFileSize(viewModel.originalSize)}\",\n                style = MaterialTheme.typography.body2,\n                color = MaterialTheme.colors.onSurface\n            )\n            Text(\n                text = run {\n                    val original = viewModel.originalSize\n                    val compressed = viewModel.compressedSize\n                    val ratioLabel = if (original > 0 && compressed > original) {\n                        i18nState.getString(\"size_increase\")\n                    } else {\n                        i18nState.getString(\"compression_ratio\")\n                    }\n                    val percent = if (original > 0) abs((100 * (1 - compressed.toDouble() / original)).toInt()) else 0\n                    \"${i18nState.getString(\"compressed\")}: ${ImageCompressionUtils.formatFileSize(compressed)} $ratioLabel: $percent%\"\n                },\n                style = MaterialTheme.typography.body2,\n                fontWeight = FontWeight.Bold,\n                color = MaterialTheme.colors.primary\n            )\n        }\n        \n        // 操作按钮（仅在单张图模式下显示）\n        if (compressionMode == CompressionMode.SINGLE) {\n            CompressionActionButtons(\n                viewModel = viewModel,\n                state = state,\n                onShowToast = onShowToast,\n                i18nState = i18nState\n            )\n        }\n    }\n}\n\n"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/compression/CompressionProgress.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.compression\n\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.material.*\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\n\n@Composable\nfun CompressionProgressSection(\n    viewModel: CompressionViewModel,\n    onCancel: () -> Unit,\n    i18nState: cn.netdiscovery.monica.ui.i18n.I18nState\n) {\n    Column(\n        verticalArrangement = Arrangement.spacedBy(8.dp)\n    ) {\n        // 压缩消息和取消按钮\n        Row(\n            modifier = Modifier.fillMaxWidth(),\n            horizontalArrangement = Arrangement.SpaceBetween,\n            verticalAlignment = Alignment.CenterVertically\n        ) {\n            // 压缩消息\n            if (viewModel.compressionMessage.isNotEmpty()) {\n                Text(\n                    text = viewModel.compressionMessage,\n                    style = MaterialTheme.typography.body2,\n                    modifier = Modifier.weight(1f),\n                    color = when {\n                        viewModel.compressionMessage.contains(i18nState.getString(\"compression_success\").replace(\"！\", \"\")) || \n                        viewModel.compressionMessage.contains(i18nState.getString(\"batch_compression_completed\").split(\"（\")[0]) -> MaterialTheme.colors.primary\n                        viewModel.compressionMessage.contains(i18nState.getString(\"compression_failed\")) || \n                        viewModel.compressionMessage.contains(i18nState.getString(\"compression_error\").split(\":\")[0]) ||\n                        viewModel.compressionMessage.contains(i18nState.getString(\"compression_cancelled\")) -> MaterialTheme.colors.error\n                        else -> MaterialTheme.colors.onSurface\n                    },\n                    fontWeight = FontWeight.Medium\n                )\n            } else {\n                Spacer(modifier = Modifier.weight(1f))\n            }\n            \n            // 取消按钮（仅在压缩中显示）\n            if (viewModel.isCompressing) {\n                Button(\n                    onClick = onCancel,\n                    modifier = Modifier.height(32.dp),\n                    colors = ButtonDefaults.buttonColors(\n                        backgroundColor = MaterialTheme.colors.error\n                    )\n                ) {\n                    Text(\n                        i18nState.getString(\"cancel\"),\n                        color = Color.White,\n                        fontSize = 12.sp\n                    )\n                }\n            }\n        }\n        \n        // 进度条\n        if (viewModel.isCompressing) {\n            LinearProgressIndicator(\n                progress = viewModel.compressionProgress,\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .height(8.dp),\n                color = MaterialTheme.colors.primary\n            )\n            \n            Text(\n                text = \"${(viewModel.compressionProgress * 100).toInt()}%\",\n                style = MaterialTheme.typography.caption,\n                color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)\n            )\n        }\n    }\n}\n\n\n"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/compression/CompressionSliders.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.compression\n\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.material.*\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.dp\nimport cn.netdiscovery.monica.ui.i18n.I18nState\n\n@Composable\nfun QualitySlider(\n    value: Float,\n    onValueChange: (Float) -> Unit,\n    i18nState: cn.netdiscovery.monica.ui.i18n.I18nState\n) {\n    Column {\n        Row(\n            modifier = Modifier.fillMaxWidth(),\n            horizontalArrangement = Arrangement.SpaceBetween,\n            verticalAlignment = Alignment.CenterVertically\n        ) {\n            Text(\n                text = i18nState.getString(\"quality_setting\"),\n                style = MaterialTheme.typography.subtitle2,\n                fontWeight = FontWeight.Bold\n            )\n            Text(\n                text = \"${(value * 100).toInt()}%\",\n                style = MaterialTheme.typography.body2,\n                color = MaterialTheme.colors.primary,\n                fontWeight = FontWeight.Bold\n            )\n        }\n        \n        Slider(\n            value = value,\n            onValueChange = onValueChange,\n            valueRange = 0f..1f,\n            modifier = Modifier.fillMaxWidth(),\n            colors = SliderDefaults.colors(\n                thumbColor = MaterialTheme.colors.primary,\n                activeTrackColor = MaterialTheme.colors.primary\n            )\n        )\n        \n        Row(\n            modifier = Modifier.fillMaxWidth(),\n            horizontalArrangement = Arrangement.SpaceBetween\n        ) {\n            Text(\"0\", style = MaterialTheme.typography.caption)\n            Text(\"${(value * 100).toInt()}%\", style = MaterialTheme.typography.caption)\n        }\n    }\n}\n\n@Composable\nfun CompressionLevelSlider(\n    value: Int,\n    onValueChange: (Int) -> Unit,\n    i18nState: cn.netdiscovery.monica.ui.i18n.I18nState\n) {\n    // 使用浮点值保持拖动时的平滑性\n    var sliderValue by remember(value) { mutableStateOf(value.toFloat()) }\n    \n    // 当外部值改变时同步更新滑块值\n    LaunchedEffect(value) {\n        sliderValue = value.toFloat()\n    }\n    \n    Column {\n        Row(\n            modifier = Modifier.fillMaxWidth(),\n            horizontalArrangement = Arrangement.SpaceBetween,\n            verticalAlignment = Alignment.CenterVertically\n        ) {\n            Text(\n                text = i18nState.getString(\"compression_level\"),\n                style = MaterialTheme.typography.subtitle2,\n                fontWeight = FontWeight.Bold\n            )\n            Text(\n                text = \"${sliderValue.toInt()}/9\",\n                style = MaterialTheme.typography.body2,\n                color = MaterialTheme.colors.primary,\n                fontWeight = FontWeight.Bold\n            )\n        }\n        \n        Slider(\n            value = sliderValue,\n            onValueChange = { newValue ->\n                // 拖动过程中保持浮点值，确保平滑\n                sliderValue = newValue.coerceIn(0f, 9f)\n            },\n            onValueChangeFinished = {\n                // 拖动结束时才转换为整数并通知外部\n                onValueChange(sliderValue.toInt().coerceIn(0, 9))\n            },\n            valueRange = 0f..9f,\n            modifier = Modifier.fillMaxWidth(),\n            colors = SliderDefaults.colors(\n                thumbColor = MaterialTheme.colors.primary,\n                activeTrackColor = MaterialTheme.colors.primary\n            )\n        )\n    }\n}\n\n"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/compression/CompressionView.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.compression\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material.*\nimport androidx.compose.runtime.*\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.dp\nimport cn.netdiscovery.monica.state.ApplicationState\nimport cn.netdiscovery.monica.ui.i18n.rememberI18nState\nimport cn.netdiscovery.monica.utils.CompressionAlgorithm\nimport cn.netdiscovery.monica.utils.ImageCompressionUtils\nimport org.slf4j.Logger\nimport org.slf4j.LoggerFactory\nimport java.io.File\n\n/**\n * 图像压缩 UI 视图\n * \n * @author: Tony Shen\n * @date: 2025/12/07\n * @version: V2.0\n */\nprivate val logger: Logger = LoggerFactory.getLogger(object : Any() {}.javaClass.enclosingClass)\n\n@Composable\nfun compressionView(state: ApplicationState) {\n    val viewModel: CompressionViewModel = remember { CompressionViewModel() }\n    val i18nState = rememberI18nState()\n    \n    var compressionMode by remember { mutableStateOf(CompressionMode.SINGLE) }\n    var selectedOutputDir by remember { mutableStateOf<File?>(null) }\n    \n    // Toast 状态（提升到顶层，使 toast 在整个页面居中）\n    var showToast by remember { mutableStateOf(false) }\n    var toastMessage by remember { mutableStateOf(\"\") }\n    \n    // 当切换模式时，重置结果\n    LaunchedEffect(compressionMode) {\n        viewModel.resetResult()\n        if (compressionMode == CompressionMode.BATCH) {\n            viewModel.selectedImage = null\n            viewModel.selectedImageFileSize = 0L\n        }\n    }\n    \n    // 显示 Toast（在整个页面居中）\n    if (showToast) {\n        cn.netdiscovery.monica.ui.widget.centerToast(\n            modifier = Modifier.fillMaxSize(),\n            message = toastMessage\n        ) {\n            showToast = false\n        }\n    }\n    \n    // 左右分栏布局\n    Row(\n        modifier = Modifier\n            .fillMaxSize()\n            .background(MaterialTheme.colors.background)\n            .padding(16.dp),\n        horizontalArrangement = Arrangement.spacedBy(16.dp)\n    ) {\n        // 左侧控制面板\n        LeftControlPanel(\n            modifier = Modifier\n                .width(400.dp)\n                .fillMaxHeight(),\n            viewModel = viewModel,\n            compressionMode = compressionMode,\n            onModeChange = { compressionMode = it },\n            selectedOutputDir = selectedOutputDir,\n            onOutputDirSelected = { selectedOutputDir = it },\n            state = state,\n            i18nState = i18nState,\n            onShowToast = { message ->\n                toastMessage = message\n                showToast = true\n            }\n        )\n        \n        // 右侧图片预览对比区域\n        CompressionRightPanel(\n            modifier = Modifier\n                .weight(1f)\n                .fillMaxHeight(),\n            state = state,\n            viewModel = viewModel,\n            compressionMode = compressionMode,\n            onShowToast = { message ->\n                toastMessage = message\n                showToast = true\n            },\n            i18nState = i18nState\n        )\n    }\n}\n\n@Composable\nprivate fun LeftControlPanel(\n    modifier: Modifier = Modifier,\n    viewModel: CompressionViewModel,\n    compressionMode: CompressionMode,\n    onModeChange: (CompressionMode) -> Unit,\n    selectedOutputDir: File?,\n    onOutputDirSelected: (File) -> Unit,\n    state: ApplicationState,\n    i18nState: cn.netdiscovery.monica.ui.i18n.I18nState,\n    onShowToast: (String) -> Unit\n) {\n    Card(\n        modifier = modifier,\n        shape = RoundedCornerShape(12.dp),\n        elevation = 4.dp,\n        backgroundColor = MaterialTheme.colors.surface\n    ) {\n        Column(\n            modifier = Modifier\n                .fillMaxSize()\n                .padding(20.dp)\n                .verticalScroll(rememberScrollState()),\n            verticalArrangement = Arrangement.spacedBy(20.dp)\n        ) {\n            // 标题\n            Row(\n                verticalAlignment = Alignment.CenterVertically,\n                horizontalArrangement = Arrangement.spacedBy(8.dp)\n            ) {\n                Icon(\n                    painter = painterResource(\"images/controlpanel/compress.png\"),\n                    contentDescription = null,\n                    modifier = Modifier.size(24.dp),\n                    tint = MaterialTheme.colors.primary\n                )\n                Text(\n                    text = i18nState.getString(\"image_compression\"),\n                    style = MaterialTheme.typography.h6,\n                    fontWeight = FontWeight.Bold,\n                    color = MaterialTheme.colors.primary\n                )\n            }\n            \n            Divider()\n            \n            // 压缩算法选择（下拉菜单样式）\n            CompressionAlgorithmDropdown(\n                viewModel = viewModel,\n                i18nState = i18nState\n            )\n            \n            // WebP 不支持提示\n            if ((viewModel.selectedAlgorithm == CompressionAlgorithm.WEBP_LOSSY ||\n                 viewModel.selectedAlgorithm == CompressionAlgorithm.WEBP_LOSSLESS) &&\n                !ImageCompressionUtils.isWebPSupported()) {\n                Card(\n                    modifier = Modifier.fillMaxWidth(),\n                    backgroundColor = MaterialTheme.colors.error.copy(alpha = 0.1f),\n                    shape = RoundedCornerShape(8.dp)\n                ) {\n                    Row(\n                        modifier = Modifier\n                            .fillMaxWidth()\n                            .padding(12.dp),\n                        verticalAlignment = Alignment.CenterVertically,\n                        horizontalArrangement = Arrangement.spacedBy(8.dp)\n                    ) {\n                        Icon(\n                            painter = painterResource(\"images/controlpanel/compress.png\"),\n                            contentDescription = null,\n                            modifier = Modifier.size(16.dp),\n                            tint = MaterialTheme.colors.error\n                        )\n                        Text(\n                            text = i18nState.getString(\"webp_not_supported_auto_convert\").format(ImageCompressionUtils.getWebPFallbackFormat(viewModel.selectedAlgorithm == CompressionAlgorithm.WEBP_LOSSY)),\n                            style = MaterialTheme.typography.caption,\n                            color = MaterialTheme.colors.error\n                        )\n                    }\n                }\n            }\n            \n            Divider()\n            \n            // 质量设置\n            when (viewModel.selectedAlgorithm) {\n                CompressionAlgorithm.JPEG_QUALITY,\n                CompressionAlgorithm.WEBP_LOSSY -> {\n                    QualitySlider(\n                        value = viewModel.quality,\n                        onValueChange = { viewModel.quality = it },\n                        i18nState = i18nState\n                    )\n                }\n                CompressionAlgorithm.PNG_OPTIMIZATION,\n                CompressionAlgorithm.WEBP_LOSSLESS -> {\n                    CompressionLevelSlider(\n                        value = viewModel.compressionLevel,\n                        onValueChange = { viewModel.compressionLevel = it },\n                        i18nState = i18nState\n                    )\n                }\n            }\n            \n            Divider()\n            \n            // 输入选择\n            CompressionInputSection(\n                compressionMode = compressionMode,\n                onModeChange = onModeChange,\n                selectedOutputDir = selectedOutputDir,\n                onOutputDirSelected = onOutputDirSelected,\n                viewModel = viewModel,\n                state = state,\n                i18nState = i18nState,\n                onShowToast = onShowToast\n            )\n            \n            // 压缩进度和消息显示\n            if (viewModel.isCompressing || viewModel.compressionMessage.isNotEmpty()) {\n                Divider()\n                CompressionProgressSection(\n                    viewModel = viewModel,\n                    onCancel = { viewModel.cancelCompression { i18nState.getString(it) } },\n                    i18nState = i18nState\n                )\n            }\n        }\n    }\n}\n\n\n/**\n * 压缩模式枚举\n */\nenum class CompressionMode(val displayName: String) {\n    SINGLE(\"单张图片\"),\n    BATCH(\"批量图片\")\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/compression/CompressionViewModel.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.compression\n\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport cn.netdiscovery.monica.state.ApplicationState\nimport cn.netdiscovery.monica.utils.*\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.isActive\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport org.slf4j.Logger\nimport org.slf4j.LoggerFactory\nimport java.io.File\nimport javax.imageio.ImageIO\n\n/**\n * 图像压缩 ViewModel\n * \n * @author: Tony Shen\n * @date: 2025/12/07\n * @version: V1.0\n */\nprivate val logger: Logger = LoggerFactory.getLogger(CompressionViewModel::class.java)\n\nclass CompressionViewModel {\n\n    companion object {\n        private val DEFAULT_ALGORITHM: CompressionAlgorithm = CompressionAlgorithm.JPEG_QUALITY\n        private const val DEFAULT_QUALITY: Float = 0.8f\n        private const val DEFAULT_COMPRESSION_LEVEL: Int = 6\n    }\n\n    private suspend fun ui(block: () -> Unit) {\n        withContext(Dispatchers.Main) { block() }\n    }\n    \n    // 压缩算法选择\n    var selectedAlgorithm by mutableStateOf(CompressionAlgorithm.JPEG_QUALITY)\n    \n    // JPEG 和 WebP Lossy 的质量参数（0.0 - 1.0）\n    var quality by mutableStateOf(0.8f)\n    \n    // PNG 和 WebP Lossless 的压缩级别（0 - 9）\n    var compressionLevel by mutableStateOf(6)\n    \n    // 压缩进度显示\n    var isCompressing by mutableStateOf(false)\n    var compressionProgress by mutableStateOf(0f)\n    var compressionMessage by mutableStateOf(\"\")\n    \n    // 压缩结果统计\n    var originalSize by mutableStateOf(0L)\n    var compressedSize by mutableStateOf(0L)\n    var compressionRatio by mutableStateOf(0)\n    var showResult by mutableStateOf(false)\n    \n    // 单张图模式下选择的图片\n    var selectedImage by mutableStateOf<java.awt.image.BufferedImage?>(null)\n    \n    // 单张图模式下选择的原始文件\n    var selectedImageFile by mutableStateOf<File?>(null)\n    \n    // 单张图模式下选择的原始文件大小\n    var selectedImageFileSize by mutableStateOf(0L)\n    \n    // 压缩后的图片\n    var compressedImage by mutableStateOf<java.awt.image.BufferedImage?>(null)\n\n    // 单张图模式：复用预览阶段的压缩结果，避免“预览压一次，保存再压一次”导致 JPG 不稳定\n    private var lastCompressedData: ByteArray? = null\n    private var lastCompressedUsedFallback: Boolean = false\n    private var lastCompressedParams: CompressionParams? = null\n    \n    // WebP 降级提示\n    var webpFallbackWarning by mutableStateOf<String?>(null)\n\n    // 压缩后文件变大提示（例如 JPG 重新编码质量更高时）\n    var sizeChangeWarning by mutableStateOf<String?>(null)\n\n    // Undo：保存“应用到编辑器”前的快照（允许为 null，兼容编辑器未加载图片）\n    private var hasAppliedSnapshot: Boolean = false\n    private var lastAppliedPrevCurrent: java.awt.image.BufferedImage? = null\n    private var lastAppliedPrevRaw: java.awt.image.BufferedImage? = null\n    private var lastAppliedPrevFile: File? = null\n    \n    // 批量压缩时的文件数\n    var totalFiles by mutableStateOf(0)\n    var processedFiles by mutableStateOf(0)\n    \n    // 压缩任务引用（用于取消）\n    private var compressionJob: Job? = null\n    \n    /**\n     * 获取当前的压缩参数\n     */\n    fun getCurrentParams(): CompressionParams {\n        return CompressionParams(\n            algorithm = selectedAlgorithm,\n            quality = quality,\n            compressionLevel = compressionLevel\n        )\n    }\n    \n    /**\n     * 压缩单张图片到预览（不保存文件）\n     */\n    fun compressSingleImageToPreview(\n        scope: CoroutineScope,\n        getString: (String) -> String\n    ) {\n        // 取消之前的任务\n        compressionJob?.cancel()\n        compressionJob = scope.launch(Dispatchers.Default) {\n            try {\n                val image = selectedImage ?: run {\n                    ui { compressionMessage = getString(\"error_please_select_image\") }\n                    return@launch\n                }\n                \n                // 更新状态（UI 线程）\n                ui {\n                    isCompressing = true\n                    compressionMessage = getString(\"compressing_image\")\n                    compressionProgress = 0.3f\n                }\n                \n                val params = getCurrentParams()\n                // 使用原始文件的实际大小\n                ui { originalSize = selectedImageFileSize }\n                \n                ui { compressionProgress = 0.6f }\n                \n                // 检查 WebP 支持\n                if ((params.algorithm == CompressionAlgorithm.WEBP_LOSSY || \n                     params.algorithm == CompressionAlgorithm.WEBP_LOSSLESS) &&\n                    !ImageCompressionUtils.isWebPSupported()) {\n                    val fallbackFormat = ImageCompressionUtils.getWebPFallbackFormat(\n                        params.algorithm == CompressionAlgorithm.WEBP_LOSSY\n                    )\n                    ui { webpFallbackWarning = getString(\"webp_not_supported\").format(fallbackFormat) }\n                } else {\n                    // 检查格式转换警告（JPG 转 PNG 等）\n                    val formatWarningKey = ImageCompressionUtils.checkFormatConversionWarning(\n                        selectedImageFile,\n                        params.algorithm\n                    )\n                    ui { webpFallbackWarning = formatWarningKey?.let { getString(it) } }\n                }\n                \n                // 压缩到内存\n                val compressedBytes = ImageCompressionUtils.compressImage(image, params)\n                \n                if (compressedBytes != null) {\n                    val (compressedData, usedFallback) = compressedBytes\n                    lastCompressedData = compressedData\n                    lastCompressedUsedFallback = usedFallback\n                    lastCompressedParams = params\n                    val localCompressedSize = compressedData.size.toLong()\n                    \n                    // 如果使用了降级处理，更新警告信息\n                    if (usedFallback && webpFallbackWarning == null) {\n                        val fallbackFormat = ImageCompressionUtils.getWebPFallbackFormat(\n                            params.algorithm == CompressionAlgorithm.WEBP_LOSSY\n                        )\n                        ui { webpFallbackWarning = getString(\"webp_encode_failed\").format(fallbackFormat) }\n                    }\n                    \n                    // 从压缩后的字节数组加载图片\n                    val compressedImageData = javax.imageio.ImageIO.read(java.io.ByteArrayInputStream(compressedData))\n                    ui {\n                        compressedSize = localCompressedSize\n                        compressionRatio = ImageCompressionUtils.calculateCompressionRatio(originalSize, compressedSize)\n                        sizeChangeWarning = if (originalSize > 0 && compressedSize >= originalSize) {\n                            getString(\"compressed_file_larger_warning\")\n                        } else {\n                            null\n                        }\n                        if (compressedImageData != null) {\n                            compressedImage = compressedImageData\n                        }\n                        compressionMessage = getString(\"compression_success\")\n                        compressionProgress = 1f\n                        showResult = true\n                    }\n                } else {\n                    ui {\n                        compressionMessage = getString(\"compression_failed\")\n                        compressionProgress = 0f\n                    }\n                }\n                ui { isCompressing = false }\n            } catch (e: Exception) {\n                logger.error(\"Single image compression error\", e)\n                ui {\n                    compressionMessage = getString(\"compression_error\").format(e.message ?: \"\")\n                    compressionProgress = 0f\n                    isCompressing = false\n                    sizeChangeWarning = null\n                }\n            }\n        }\n        compressionJob?.invokeOnCompletion {\n            compressionJob = null\n        }\n    }\n    \n    /**\n     * 批量压缩文件夹中的所有图片\n     * 使用流式处理优化内存使用\n     */\n    fun compressBatch(\n        sourceDir: File,\n        outputDir: File,\n        scope: CoroutineScope,\n        getString: (String) -> String\n    ) {\n        // 取消之前的任务\n        compressionJob?.cancel()\n        compressionJob = scope.launch(Dispatchers.Default) {\n            try {\n                ui {\n                    isCompressing = true\n                    compressionMessage = getString(\"preparing_batch_compression\")\n                    compressionProgress = 0f\n                }\n                \n                val params = getCurrentParams()\n                val imageExtensions = setOf(\"jpg\", \"jpeg\", \"png\", \"bmp\", \"gif\", \"tiff\")\n                \n                // 先统计文件数量（用于进度显示）\n                var fileCount = 0\n                sourceDir.walk().forEach { file ->\n                    if (file.isFile && file.extension.lowercase() in imageExtensions) {\n                        fileCount++\n                    }\n                }\n                \n                ui {\n                    totalFiles = fileCount\n                    processedFiles = 0\n                }\n                \n                if (totalFiles == 0) {\n                    ui {\n                        compressionMessage = getString(\"no_images_in_folder\")\n                        isCompressing = false\n                    }\n                    return@launch\n                }\n                \n                if (!outputDir.exists()) {\n                    outputDir.mkdirs()\n                }\n                \n                var totalOriginalSize = 0L\n                var totalCompressedSize = 0L\n                \n                // 使用流式处理，逐个处理文件，避免一次性加载所有文件到内存\n                sourceDir.walk().forEach { file ->\n                    // 检查是否已取消\n                    if (!isActive) {\n                        ui { compressionMessage = getString(\"compression_cancelled\") }\n                        return@forEach\n                    }\n                    \n                    if (!file.isFile || file.extension.lowercase() !in imageExtensions) {\n                        return@forEach\n                    }\n                    \n                    try {\n                        ui {\n                            processedFiles++\n                            compressionMessage = getString(\"compressing_file\").format(file.name, processedFiles, totalFiles)\n                            compressionProgress = processedFiles.toFloat() / totalFiles\n                        }\n                        \n                        // 读取图片（处理完立即释放）\n                        val image = ImageIO.read(file) ?: run {\n                            return@forEach\n                        }\n                        \n                        val baseName = file.nameWithoutExtension\n                        val outputFileName = \"$baseName.${params.algorithm.format}\"\n                        val outputFile = File(outputDir, outputFileName)\n                        \n                        val fileOriginalSize = file.length()\n                        \n                        val result = ImageCompressionUtils.compressAndSaveImage(image, outputFile, params)\n                        \n                        // 立即释放图片内存\n                        image.flush()\n                        \n                        if (result != null) {\n                            val savedSize = result.sizeBytes\n                            totalOriginalSize += fileOriginalSize\n                            totalCompressedSize += savedSize\n                        }\n                        \n                    } catch (e: Exception) {\n                        logger.error(\"File processing error: ${file.absolutePath}\", e)\n                    }\n                }\n                \n                ui {\n                    originalSize = totalOriginalSize\n                    compressedSize = totalCompressedSize\n                    compressionRatio = ImageCompressionUtils.calculateCompressionRatio(totalOriginalSize, totalCompressedSize)\n                    sizeChangeWarning = if (totalOriginalSize > 0 && totalCompressedSize >= totalOriginalSize) {\n                        getString(\"compressed_file_larger_warning\")\n                    } else {\n                        null\n                    }\n                    compressionMessage = getString(\"batch_compression_completed\").format(processedFiles, totalFiles)\n                    showResult = true\n                }\n                \n            } catch (e: Exception) {\n                logger.error(\"Batch compression error\", e)\n                ui {\n                    compressionMessage = getString(\"batch_compression_error\").format(e.message ?: \"\")\n                    sizeChangeWarning = null\n                }\n            } finally {\n                ui { isCompressing = false }\n                compressionJob = null\n            }\n        }\n    }\n    \n    /**\n     * 取消压缩任务\n     */\n    fun cancelCompression(getString: (String) -> String) {\n        compressionJob?.cancel()\n        compressionJob = null\n        isCompressing = false\n        compressionMessage = getString(\"compression_cancelled\")\n    }\n    \n    /**\n     * 重置压缩结果（切换模式时调用）\n     */\n    fun resetResult() {\n        // 取消正在进行的任务（静默）\n        compressionJob?.cancel()\n        compressionJob = null\n        isCompressing = false\n\n        showResult = false\n        // 注意：不清空 originalSize，因为单张图模式下它应该保留原始文件大小\n        // 批量模式下 originalSize 会在 compressBatch 中重新计算\n        compressedSize = 0L\n        compressionRatio = 0\n        processedFiles = 0\n        totalFiles = 0\n        compressionProgress = 0f\n        compressionMessage = \"\"\n        compressedImage = null\n        webpFallbackWarning = null\n        sizeChangeWarning = null\n        lastCompressedData = null\n        lastCompressedUsedFallback = false\n        lastCompressedParams = null\n    }\n\n    /**\n     * Reset 语义：重置参数 + 清掉压缩结果\n     */\n    fun resetAll() {\n        selectedAlgorithm = DEFAULT_ALGORITHM\n        quality = DEFAULT_QUALITY\n        compressionLevel = DEFAULT_COMPRESSION_LEVEL\n        resetResult()\n    }\n    \n    /**\n     * 清除当前加载的图像（用于切换到另一张图片）\n     */\n    fun clearSelectedImage() {\n        selectedImage = null\n        selectedImageFile = null\n        selectedImageFileSize = 0L\n        resetResult()\n    }\n\n    fun isAtDefaultParams(): Boolean {\n        return selectedAlgorithm == DEFAULT_ALGORITHM &&\n            kotlin.math.abs(quality - DEFAULT_QUALITY) < 0.0001f &&\n            compressionLevel == DEFAULT_COMPRESSION_LEVEL\n    }\n\n    fun saveLastCompressedToFile(outputFile: File): ImageCompressionUtils.SaveResult? {\n        val data = lastCompressedData ?: return null\n        val params = lastCompressedParams ?: return null\n        return ImageCompressionUtils.saveCompressedData(\n            outputFile = outputFile,\n            params = params,\n            compressedData = data,\n            usedFallback = lastCompressedUsedFallback\n        )\n    }\n    \n    /**\n     * 应用压缩后的图片到编辑器\n     */\n    fun applyCompressedImage(state: ApplicationState) {\n        val image = compressedImage ?: return\n\n        // 保存 Apply 前快照：用于压缩模块 Undo（不依赖全局队列）\n        hasAppliedSnapshot = true\n        lastAppliedPrevCurrent = state.currentImage\n        lastAppliedPrevRaw = state.rawImage\n        lastAppliedPrevFile = state.rawImageFile\n        \n        // 同时更新 rawImage 和 currentImage，保持与其他地方的一致性\n        state.rawImage = image\n        state.currentImage = image\n        // 压缩后的图片没有原始文件，设置为 null\n        state.rawImageFile = null\n    }\n\n    fun undoApplied(state: ApplicationState): Boolean {\n        if (!hasAppliedSnapshot) return false\n\n        state.rawImage = lastAppliedPrevRaw\n        state.currentImage = lastAppliedPrevCurrent\n        state.rawImageFile = lastAppliedPrevFile\n\n        hasAppliedSnapshot = false\n        lastAppliedPrevCurrent = null\n        lastAppliedPrevRaw = null\n        lastAppliedPrevFile = null\n        return true\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/cropimage/CropAgent.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.cropimage\n\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.geometry.Rect\nimport androidx.compose.ui.graphics.*\nimport androidx.compose.ui.unit.Density\nimport androidx.compose.ui.unit.LayoutDirection\nimport cn.netdiscovery.monica.imageprocess.utils.extension.resize\nimport cn.netdiscovery.monica.imageprocess.utils.extension.subImage\nimport cn.netdiscovery.monica.ui.controlpanel.cropimage.model.CropImageMask\nimport cn.netdiscovery.monica.ui.controlpanel.cropimage.model.CropOutline\nimport cn.netdiscovery.monica.ui.controlpanel.cropimage.model.CropPath\nimport cn.netdiscovery.monica.ui.controlpanel.cropimage.model.CropShape\nimport org.jetbrains.skia.Matrix33\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.cropimage.CropAgent\n * @author: Tony Shen\n * @date: 2024/5/26 15:45\n * @version: V1.0 <描述当前版本功能>\n */\nclass CropAgent {\n\n    private val imagePaint = Paint().apply {\n        blendMode = BlendMode.SrcIn\n    }\n\n    private val paint = Paint()\n\n    fun crop(\n        imageBitmap: ImageBitmap,\n        cropRect: Rect,\n        cropOutline: CropOutline,\n        layoutDirection: LayoutDirection,\n        density: Density,\n    ): ImageBitmap {\n\n        val imageToCrop = imageBitmap.toAwtImage().subImage(cropRect.left.toInt(),cropRect.top.toInt(),cropRect.width.toInt(),cropRect.height.toInt()).toComposeImageBitmap()\n\n        drawCroppedImage(cropOutline, cropRect, layoutDirection, density, imageToCrop)\n\n        return imageToCrop\n    }\n\n    private fun drawCroppedImage(\n        cropOutline: CropOutline,\n        cropRect: Rect,\n        layoutDirection: LayoutDirection,\n        density: Density,\n        imageToCrop: ImageBitmap,\n    ) {\n\n        when (cropOutline) {\n            is CropShape -> {\n\n                val path = Path().apply {\n                    val outline = cropOutline.shape.createOutline(cropRect.size, layoutDirection, density)\n                    addOutline(outline)\n                }\n\n                Canvas(image = imageToCrop).run {\n                    saveLayer(cropRect, imagePaint)\n\n                    // Destination\n                    drawPath(path, paint)\n\n                    // Source\n                    drawImage(\n                        image = imageToCrop,\n                        topLeftOffset = Offset.Zero,\n                        paint = imagePaint\n                    )\n                    restore()\n                }\n            }\n            is CropPath -> {\n\n                val path = Path().apply {\n\n                    addPath(cropOutline.path)\n\n                    val pathSize = getBounds().size\n                    val rectSize = cropRect.size\n\n                    val matrix = Matrix33.makeScale(\n                        rectSize.width / pathSize.width,\n                        cropRect.height / pathSize.height\n                    )\n                    this.asSkiaPath().transform(matrix)\n\n                    val left = getBounds().left\n                    val top = getBounds().top\n\n                    translate(Offset(-left, -top))\n                }\n\n                Canvas(image = imageToCrop).run {\n                    saveLayer(cropRect, imagePaint)\n\n                    // Destination\n                    drawPath(path, paint)\n\n                    // Source\n                    drawImage(image = imageToCrop, topLeftOffset = Offset.Zero, imagePaint)\n                    restore()\n                }\n            }\n            is CropImageMask -> {\n\n                val imageMask = cropOutline.image.toAwtImage().subImage(cropRect.left.toInt(),cropRect.top.toInt(),cropRect.width.toInt(),cropRect.height.toInt()).toComposeImageBitmap()\n\n                Canvas(image = imageToCrop).run {\n                    saveLayer(cropRect, imagePaint)\n\n                    // Destination\n                    drawImage(imageMask, topLeftOffset = Offset.Zero, paint)\n\n                    // Source\n                    drawImage(image = imageToCrop, topLeftOffset = Offset.Zero, imagePaint)\n\n                    restore()\n                }\n            }\n        }\n    }\n\n    fun resize(\n        croppedImageBitmap: ImageBitmap,\n        requiredWidth: Int,\n        requiredHeight: Int\n    ): ImageBitmap = croppedImageBitmap.toAwtImage().resize(requiredWidth,requiredHeight).toComposeImageBitmap()\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/cropimage/CropImageSettingView.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.cropimage\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.material.*\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport cn.netdiscovery.monica.ui.controlpanel.cropimage.setting.CropFrameFactory\nimport cn.netdiscovery.monica.ui.controlpanel.cropimage.model.aspectRatios\nimport cn.netdiscovery.monica.ui.controlpanel.cropimage.setting.CropOutlineProperty\nimport cn.netdiscovery.monica.ui.controlpanel.cropimage.setting.CropProperties\nimport cn.netdiscovery.monica.ui.widget.desktopLazyRow\nimport cn.netdiscovery.monica.ui.widget.subTitle\nimport cn.netdiscovery.monica.utils.OnCropPropertiesChange\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.cropimage.CropImageSettingView\n * @author: Tony Shen\n * @date:  2024/6/10 21:32\n * @version: V1.0 <描述当前版本功能>\n */\n\n@Composable\nfun cropTypeSelect(cropProperties: CropProperties,\n                   onCropPropertiesChange: OnCropPropertiesChange) {\n    subTitle(text = \"Crop Type\", fontWeight = FontWeight.Bold)\n\n    var expanded by remember { mutableStateOf(false) }\n\n    Column {\n        Button(modifier = Modifier.width(180.dp).padding(top = 16.dp),\n            onClick = { expanded = true },\n            enabled = true){\n\n            Text(text = cropTypes[cropTypesIndex.value].name,\n                fontSize = 22.sp,\n                color = Color.LightGray)\n        }\n\n        DropdownMenu(expanded= expanded, onDismissRequest = {expanded =false}){\n            cropTypes.forEachIndexed{ index, label ->\n                DropdownMenuItem(onClick = {\n                    cropTypesIndex.value = index\n\n                    onCropPropertiesChange.invoke(cropProperties.copy(cropType = cropTypes[cropTypesIndex.value]))\n\n                    expanded = false\n                }){\n                    Text(text = label.name)\n                }\n            }\n        }\n    }\n}\n\n@Composable\nfun contentScaleSelect(cropProperties: CropProperties,\n                       onCropPropertiesChange: OnCropPropertiesChange) {\n    subTitle(text = \"Content Scale\", fontWeight = FontWeight.Bold)\n\n    var expanded by remember { mutableStateOf(false) }\n\n    Column {\n        Button(modifier = Modifier.width(180.dp).padding(top = 16.dp),\n            onClick = { expanded = true },\n            enabled = true){\n\n            Text(text = contentScales[contentScalesIndex.value],\n                fontSize = 22.sp,\n                color = Color.LightGray)\n        }\n\n        DropdownMenu(expanded= expanded, onDismissRequest = {expanded =false}){\n            contentScales.forEachIndexed{ index, label ->\n                DropdownMenuItem(onClick = {\n                    contentScalesIndex.value = index\n\n                    val scale = when (index) {\n                        0 -> ContentScale.None\n                        1 -> ContentScale.Fit\n                        2 -> ContentScale.Crop\n                        3 -> ContentScale.FillBounds\n                        4 -> ContentScale.FillWidth\n                        5 -> ContentScale.FillHeight\n                        else -> ContentScale.Inside\n                    }\n\n                    onCropPropertiesChange.invoke(cropProperties.copy(contentScale = scale))\n\n                    expanded = false\n                }){\n                    Text(text = label)\n                }\n            }\n        }\n    }\n}\n\n\n@Composable\nfun aspectRatioScrollableRow(cropProperties: CropProperties,\n                             onCropPropertiesChange: OnCropPropertiesChange) {\n\n    var selectRadio  = remember { mutableStateOf(\"Original\") }\n\n    subTitle(text = \"Aspect Ratio (${selectRadio.value})\", fontWeight = FontWeight.Bold)\n\n    desktopLazyRow {\n        Card(\n            elevation = 16.dp,\n            modifier = Modifier.padding(start = 5.dp, top = 16.dp,end = 16.dp,bottom = 16.dp).clickable{\n                selectRadio.value = \"Original\"\n                onCropPropertiesChange.invoke(cropProperties.copy(aspectRatio = aspectRatios[0].aspectRatio))\n            }\n        ) {\n            Text(\n                text = \"Original\",\n                fontSize = 22.sp,\n                fontWeight = FontWeight.Bold,\n                modifier = Modifier\n                    .padding(16.dp)\n                    .fillMaxWidth()\n            )\n        }\n\n        Card(\n            elevation = 16.dp,\n            modifier = Modifier.padding(16.dp).clickable {\n                selectRadio.value = \"9:16\"\n                onCropPropertiesChange.invoke(cropProperties.copy(aspectRatio = aspectRatios[1].aspectRatio))\n            }\n        ) {\n            Text(\n                text = \"9:16\",\n                fontSize = 22.sp,\n                fontWeight = FontWeight.Bold,\n                modifier = Modifier\n                    .padding(16.dp)\n                    .fillMaxWidth()\n            )\n        }\n\n        Card(\n            elevation = 16.dp,\n            modifier = Modifier.padding(16.dp).clickable {\n                selectRadio.value = \"2:3\"\n                onCropPropertiesChange.invoke(cropProperties.copy(aspectRatio = aspectRatios[2].aspectRatio))\n            }\n        ) {\n            Text(\n                text = \"2:3\",\n                fontSize = 22.sp,\n                fontWeight = FontWeight.Bold,\n                modifier = Modifier\n                    .padding(16.dp)\n                    .fillMaxWidth()\n            )\n        }\n\n        Card(\n            elevation = 16.dp,\n            modifier = Modifier.padding(16.dp).clickable {\n                selectRadio.value = \"1:1\"\n                onCropPropertiesChange.invoke(cropProperties.copy(aspectRatio = aspectRatios[3].aspectRatio))\n            }\n        ) {\n            Text(\n                text = \"1:1\",\n                fontSize = 22.sp,\n                fontWeight = FontWeight.Bold,\n                modifier = Modifier\n                    .padding(16.dp)\n                    .fillMaxWidth()\n            )\n        }\n\n        Card(\n            elevation = 16.dp,\n            modifier = Modifier.padding(16.dp).clickable {\n                selectRadio.value = \"16:9\"\n                onCropPropertiesChange.invoke(cropProperties.copy(aspectRatio = aspectRatios[4].aspectRatio))\n            }\n        ) {\n            Text(\n                text = \"16:9\",\n                fontSize = 22.sp,\n                fontWeight = FontWeight.Bold,\n                modifier = Modifier\n                    .padding(16.dp)\n                    .fillMaxWidth()\n            )\n        }\n\n        Card(\n            elevation = 16.dp,\n            modifier = Modifier.padding(16.dp).clickable {\n                selectRadio.value = \"1.91:1\"\n                onCropPropertiesChange.invoke(cropProperties.copy(aspectRatio = aspectRatios[5].aspectRatio))\n            }\n        ) {\n            Text(\n                text = \"1.91:1\",\n                fontSize = 22.sp,\n                fontWeight = FontWeight.Bold,\n                modifier = Modifier\n                    .padding(16.dp)\n                    .fillMaxWidth()\n            )\n        }\n\n        Card(\n            elevation = 16.dp,\n            modifier = Modifier.padding(16.dp).clickable {\n                selectRadio.value = \"3:2\"\n                onCropPropertiesChange.invoke(cropProperties.copy(aspectRatio = aspectRatios[6].aspectRatio))\n            }\n        ) {\n            Text(\n                text = \"3:2\",\n                fontSize = 22.sp,\n                fontWeight = FontWeight.Bold,\n                modifier = Modifier\n                    .padding(16.dp)\n                    .fillMaxWidth()\n            )\n        }\n\n        Card(\n            elevation = 16.dp,\n            modifier = Modifier.padding(16.dp).clickable {\n                selectRadio.value = \"3:4\"\n                onCropPropertiesChange.invoke(cropProperties.copy(aspectRatio = aspectRatios[7].aspectRatio))\n            }\n        ) {\n            Text(\n                text = \"3:4\",\n                fontSize = 22.sp,\n                fontWeight = FontWeight.Bold,\n                modifier = Modifier\n                    .padding(16.dp)\n                    .fillMaxWidth()\n            )\n        }\n\n        Card(\n            elevation = 16.dp,\n            modifier = Modifier.padding(16.dp).clickable {\n                selectRadio.value = \"3:5\"\n                onCropPropertiesChange.invoke(cropProperties.copy(aspectRatio = aspectRatios[8].aspectRatio))\n            }\n        ) {\n            Text(\n                text = \"3:5\",\n                fontSize = 22.sp,\n                fontWeight = FontWeight.Bold,\n                modifier = Modifier\n                    .padding(16.dp)\n                    .fillMaxWidth()\n            )\n        }\n    }\n}\n\n@Composable\nfun cropFrameScrollableRow(cropProperties: CropProperties, cropFrameFactory: CropFrameFactory,\n                           onCropPropertiesChange: OnCropPropertiesChange) {\n\n    var selectCropFrame  = remember { mutableStateOf(\"Rect\") }\n\n    val cropFrames = cropFrameFactory.getCropFrames()\n\n    subTitle(text = \"Crop Frame (${selectCropFrame.value})\", fontWeight = FontWeight.Bold)\n\n    desktopLazyRow {\n        Card(\n            elevation = 16.dp,\n            modifier = Modifier.padding(start = 5.dp, top = 16.dp, end = 16.dp, bottom = 16.dp).clickable {\n                selectCropFrame.value = \"Rect\"\n                val cropFrame = cropFrames[0]\n                val cropOutlineProperty =\n                    CropOutlineProperty(cropFrame.outlineType, cropFrame.cropOutlineContainer.selectedItem)\n                onCropPropertiesChange.invoke(cropProperties.copy(cropOutlineProperty = cropOutlineProperty))\n            }\n        ) {\n            Text(\n                text = \"Rect\",\n                fontSize = 22.sp,\n                fontWeight = FontWeight.Bold,\n                modifier = Modifier\n                    .padding(16.dp)\n                    .fillMaxWidth()\n            )\n        }\n\n        Card(\n            elevation = 16.dp,\n            modifier = Modifier.padding(16.dp).clickable {\n                selectCropFrame.value = \"RoundedRect\"\n                val cropFrame = cropFrames[1]\n                val cropOutlineProperty =\n                    CropOutlineProperty(cropFrame.outlineType, cropFrame.cropOutlineContainer.selectedItem)\n                onCropPropertiesChange.invoke(cropProperties.copy(cropOutlineProperty = cropOutlineProperty))\n            }\n        ) {\n            Text(\n                text = \"RoundedRect\",\n                fontSize = 22.sp,\n                fontWeight = FontWeight.Bold,\n                modifier = Modifier\n                    .padding(16.dp)\n                    .fillMaxWidth()\n            )\n        }\n\n        Card(\n            elevation = 16.dp,\n            modifier = Modifier.padding(16.dp).clickable {\n                selectCropFrame.value = \"CutCorner\"\n                val cropFrame = cropFrames[2]\n                val cropOutlineProperty =\n                    CropOutlineProperty(cropFrame.outlineType, cropFrame.cropOutlineContainer.selectedItem)\n                onCropPropertiesChange.invoke(cropProperties.copy(cropOutlineProperty = cropOutlineProperty))\n            }\n        ) {\n            Text(\n                text = \"CutCorner\",\n                fontSize = 22.sp,\n                fontWeight = FontWeight.Bold,\n                modifier = Modifier\n                    .padding(16.dp)\n                    .fillMaxWidth()\n            )\n        }\n\n        Card(\n            elevation = 16.dp,\n            modifier = Modifier.padding(16.dp).clickable {\n                selectCropFrame.value = \"Oval\"\n                val cropFrame = cropFrames[3]\n                val cropOutlineProperty =\n                    CropOutlineProperty(cropFrame.outlineType, cropFrame.cropOutlineContainer.selectedItem)\n                onCropPropertiesChange.invoke(cropProperties.copy(cropOutlineProperty = cropOutlineProperty))\n            }\n        ) {\n            Text(\n                text = \"Oval\",\n                fontSize = 22.sp,\n                fontWeight = FontWeight.Bold,\n                modifier = Modifier\n                    .padding(16.dp)\n                    .fillMaxWidth()\n            )\n        }\n\n        Card(\n            elevation = 16.dp,\n            modifier = Modifier.padding(16.dp).clickable {\n                selectCropFrame.value = \"Triangle\"\n                val cropFrame = cropFrames[4]\n                val cropOutlineProperty =\n                    CropOutlineProperty(cropFrame.outlineType, cropFrame.cropOutlineContainer.outlines[1])\n                onCropPropertiesChange.invoke(cropProperties.copy(cropOutlineProperty = cropOutlineProperty))\n            }\n        ) {\n            Text(\n                text = \"Triangle\",\n                fontSize = 22.sp,\n                fontWeight = FontWeight.Bold,\n                modifier = Modifier\n                    .padding(16.dp)\n                    .fillMaxWidth()\n            )\n        }\n\n        Card(\n            elevation = 16.dp,\n            modifier = Modifier.padding(16.dp).clickable {\n                selectCropFrame.value = \"Polygon\"\n                val cropFrame = cropFrames[4]\n                val cropOutlineProperty =\n                    CropOutlineProperty(cropFrame.outlineType, cropFrame.cropOutlineContainer.selectedItem)\n                onCropPropertiesChange.invoke(cropProperties.copy(cropOutlineProperty = cropOutlineProperty))\n            }\n        ) {\n            Text(\n                text = \"Polygon\",\n                fontSize = 22.sp,\n                fontWeight = FontWeight.Bold,\n                modifier = Modifier\n                    .padding(16.dp)\n                    .fillMaxWidth()\n            )\n        }\n\n        Card(\n            elevation = 16.dp,\n            modifier = Modifier.padding(16.dp).clickable {\n                selectCropFrame.value = \"Parallelogram\"\n                val cropFrame = cropFrames[5]\n                val cropOutlineProperty =\n                    CropOutlineProperty(cropFrame.outlineType, cropFrame.cropOutlineContainer.selectedItem)\n                onCropPropertiesChange.invoke(cropProperties.copy(cropOutlineProperty = cropOutlineProperty))\n            }\n        ) {\n            Text(\n                text = \"Parallelogram\",\n                fontSize = 22.sp,\n                fontWeight = FontWeight.Bold,\n                modifier = Modifier\n                    .padding(16.dp)\n                    .fillMaxWidth()\n            )\n        }\n\n        Card(\n            elevation = 16.dp,\n            modifier = Modifier.padding(16.dp).clickable {\n                selectCropFrame.value = \"Diamond\"\n                val cropFrame = cropFrames[6]\n                val cropOutlineProperty =\n                    CropOutlineProperty(cropFrame.outlineType, cropFrame.cropOutlineContainer.selectedItem)\n                onCropPropertiesChange.invoke(cropProperties.copy(cropOutlineProperty = cropOutlineProperty))\n            }\n        ) {\n            Text(\n                text = \"Diamond\",\n                fontSize = 22.sp,\n                fontWeight = FontWeight.Bold,\n                modifier = Modifier\n                    .padding(16.dp)\n                    .fillMaxWidth()\n            )\n        }\n\n        Card(\n            elevation = 16.dp,\n            modifier = Modifier.padding(16.dp).clickable {\n                selectCropFrame.value = \"Ticket\"\n                val cropFrame = cropFrames[7]\n                val cropOutlineProperty =\n                    CropOutlineProperty(cropFrame.outlineType, cropFrame.cropOutlineContainer.selectedItem)\n                onCropPropertiesChange.invoke(cropProperties.copy(cropOutlineProperty = cropOutlineProperty))\n            }\n        ) {\n            Text(\n                text = \"Ticket\",\n                fontSize = 22.sp,\n                fontWeight = FontWeight.Bold,\n                modifier = Modifier\n                    .padding(16.dp)\n                    .fillMaxWidth()\n            )\n        }\n\n        Card(\n            elevation = 16.dp,\n            modifier = Modifier.padding(16.dp).clickable {\n                selectCropFrame.value = \"Heart\"\n                val cropFrame = cropFrames[8]\n                val cropOutlineProperty =\n                    CropOutlineProperty(cropFrame.outlineType, cropFrame.cropOutlineContainer.selectedItem)\n                onCropPropertiesChange.invoke(cropProperties.copy(cropOutlineProperty = cropOutlineProperty))\n            }\n        ) {\n            Text(\n                text = \"Heart\",\n                fontSize = 22.sp,\n                fontWeight = FontWeight.Bold,\n                modifier = Modifier\n                    .padding(16.dp)\n                    .fillMaxWidth()\n            )\n        }\n\n        Card(\n            elevation = 16.dp,\n            modifier = Modifier.padding(16.dp).clickable {\n                selectCropFrame.value = \"Star\"\n                val cropFrame = cropFrames[8]\n                val cropOutlineProperty =\n                    CropOutlineProperty(cropFrame.outlineType, cropFrame.cropOutlineContainer.outlines[1])\n                onCropPropertiesChange.invoke(cropProperties.copy(cropOutlineProperty = cropOutlineProperty))\n            }\n        ) {\n            Text(\n                text = \"Star\",\n                fontSize = 22.sp,\n                fontWeight = FontWeight.Bold,\n                modifier = Modifier\n                    .padding(16.dp)\n                    .fillMaxWidth()\n            )\n        }\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/cropimage/CropImageView.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.cropimage\n\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.material.AlertDialog\nimport androidx.compose.material.MaterialTheme\nimport androidx.compose.material.Text\nimport androidx.compose.material.TextButton\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.ImageBitmap\nimport androidx.compose.ui.graphics.toAwtImage\nimport androidx.compose.ui.graphics.toComposeImageBitmap\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport cn.netdiscovery.monica.state.ApplicationState\nimport cn.netdiscovery.monica.ui.controlpanel.cropimage.model.OutlineType\nimport cn.netdiscovery.monica.ui.controlpanel.cropimage.model.RectCropShape\nimport cn.netdiscovery.monica.ui.controlpanel.cropimage.setting.*\nimport cn.netdiscovery.monica.ui.widget.PageLifecycle\nimport cn.netdiscovery.monica.ui.widget.rightSideMenuBar\nimport cn.netdiscovery.monica.ui.widget.toolTipButton\nimport cn.netdiscovery.monica.utils.OnCropPropertiesChange\nimport org.koin.compose.koinInject\nimport org.slf4j.Logger\nimport org.slf4j.LoggerFactory\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.cropimage.CropImageView\n * @author: Tony Shen\n * @date: 2024/5/27 14:00\n * @version: V1.0 <描述当前版本功能>\n */\nprivate val logger: Logger = LoggerFactory.getLogger(object : Any() {}.javaClass.enclosingClass)\n\nval cropTypes = mutableListOf(CropType.Dynamic, CropType.Static)\nvar cropTypesIndex = mutableStateOf(0)\n\nval contentScales = listOf(\"None\", \"Fit\", \"Crop\", \"FillBounds\", \"FillWidth\", \"FillHeight\", \"Inside\")\nvar contentScalesIndex = mutableStateOf(1)\n\n@Composable\nfun cropImage(state: ApplicationState) {\n\n    val cropViewModel: CropViewModel = koinInject()\n\n    val handleSize: Float = LocalDensity.current.run { 20.dp.toPx() }\n    var croppedImage by remember { mutableStateOf<ImageBitmap?>(null) }\n    var crop by remember { mutableStateOf(false) }\n\n    var showSettingDialog by remember { mutableStateOf(false) }\n    var showCropDialog by remember { mutableStateOf(false) }\n    var isCropping by remember { mutableStateOf(false) }\n\n    var cropProperties by remember {\n        mutableStateOf(\n            CropDefaults.properties(\n                cropOutlineProperty = CropOutlineProperty(\n                    OutlineType.Rect,\n                    RectCropShape(0, \"Rect\")\n                ),\n                handleSize = handleSize\n            )\n        )\n    }\n    var cropStyle by remember { mutableStateOf(CropDefaults.style()) }\n\n    val imageBitmap = state.currentImage!!.toComposeImageBitmap()\n\n    val cropFrameFactory = remember {\n        CropFrameFactory(\n            listOf(imageBitmap)\n        )\n    }\n\n    PageLifecycle(\n        onInit = {\n            logger.info(\"CropImageView 启动时初始化\")\n        },\n        onDisposeEffect = {\n            logger.info(\"CropImageView 关闭时释放资源\")\n            cropViewModel.clearCropImageView()\n        }\n    )\n\n    Box(\n        modifier = Modifier\n            .fillMaxSize()\n            .background(Color.DarkGray),\n        contentAlignment = Alignment.Center\n    ) {\n        Column(modifier = Modifier.fillMaxSize()) {\n\n            ImageCropper(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .weight(1f),\n                imageBitmap = imageBitmap,\n                contentDescription = \"Image Cropper\",\n                cropStyle = cropStyle,\n                cropProperties = cropProperties,\n                crop = crop,\n                onCropStart = {\n                    isCropping = true\n                },\n                onCropSuccess = {\n                    croppedImage = it\n                    isCropping = false\n                    crop = false\n                    showCropDialog = true\n                }\n            )\n        }\n\n        rightSideMenuBar(modifier = Modifier.align(Alignment.CenterEnd)) {\n            toolTipButton(text = \"settings\",\n                painter = painterResource(\"images/cropimage/settings.png\"),\n                onClick = {\n                    showSettingDialog = true\n                })\n\n            toolTipButton(text = \"crop\",\n                painter = painterResource(\"images/cropimage/crop.png\"),\n                onClick = {\n                    crop = true\n                })\n        }\n    }\n\n    if (showSettingDialog) {\n        showCroppedImageSettingDialog(\n            cropProperties, cropFrameFactory,\n            onConfirm = {\n                cropProperties = it\n\n                showSettingDialog = false\n            },\n            onDismiss = {\n                showSettingDialog = false\n            }\n        )\n    }\n\n    if (showCropDialog) {\n        croppedImage?.let {\n            showCroppedImageDialog(imageBitmap = it,\n            onConfirm = {\n                showCropDialog = !showCropDialog\n\n                state.addQueue(state.currentImage!!)\n                state.currentImage = it.toAwtImage()\n                state.closePreviewWindow()\n\n                croppedImage = null\n            }, onDismiss = {\n                showCropDialog = !showCropDialog\n                croppedImage = null\n            })\n        }\n    }\n}\n\n@Composable\nprivate fun showCroppedImageSettingDialog(cropProperties: CropProperties,\n                                          cropFrameFactory: CropFrameFactory,\n                                          onConfirm: OnCropPropertiesChange,\n                                          onDismiss: () -> Unit) {\n\n    var tempProperties: CropProperties = cropProperties\n\n    AlertDialog(\n        onDismissRequest = onDismiss,\n        text = {\n            Column(\n                verticalArrangement = Arrangement.Center\n            ) {\n                Text(\n                    modifier = Modifier.align(Alignment.CenterHorizontally),\n                    text = \"Crop Properties Settings\",\n                    color = MaterialTheme.colors.primary,\n                    fontSize = 32.sp,\n                    fontWeight = FontWeight.Bold\n                )\n\n                divider()\n\n                cropTypeSelect(tempProperties) {\n                    tempProperties = it\n                }\n\n                divider()\n\n                contentScaleSelect(tempProperties) {\n                    tempProperties = it\n                }\n\n                divider()\n\n                aspectRatioScrollableRow(tempProperties) {\n                    tempProperties = it\n                }\n\n                divider()\n\n                cropFrameScrollableRow(tempProperties,cropFrameFactory) {\n                    tempProperties = it\n                }\n            }\n        },\n        confirmButton = {\n            TextButton(\n                onClick = {\n                    onConfirm(tempProperties)\n                }\n            ) {\n                Text(\"Confirm\")\n            }\n        },\n        dismissButton = {\n            TextButton(\n                onClick = {\n                    onDismiss()\n                }\n            ) {\n                Text(\"Dismiss\")\n            }\n        }\n    )\n}\n\n@Composable\nprivate fun showCroppedImageDialog(imageBitmap: ImageBitmap,\n                                   onConfirm: () -> Unit,\n                                   onDismiss: () -> Unit) {\n    AlertDialog(\n        onDismissRequest = onDismiss,\n        text = {\n            Image(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .aspectRatio(1f),\n                contentScale = ContentScale.Fit,\n                bitmap = imageBitmap,\n                contentDescription = \"result\"\n            )\n        },\n        confirmButton = {\n            TextButton(\n                onClick = {\n                    onConfirm()\n                }\n            ) {\n                Text(\"Confirm\")\n            }\n        },\n        dismissButton = {\n            TextButton(\n                onClick = {\n                    onDismiss()\n                }\n            ) {\n                Text(\"Dismiss\")\n            }\n        }\n    )\n}\n\n@Composable\nprivate fun divider() {\n    Spacer(modifier = Modifier.padding(top = 15.dp, bottom = 15.dp))\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/cropimage/CropModifier.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.cropimage\n\nimport androidx.compose.foundation.gestures.detectTapGestures\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.composed\nimport androidx.compose.ui.draw.clipToBounds\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.graphics.graphicsLayer\nimport androidx.compose.ui.input.pointer.pointerInput\nimport androidx.compose.ui.platform.debugInspectorInfo\nimport cn.netdiscovery.monica.ui.controlpanel.cropimage.state.CropData\nimport cn.netdiscovery.monica.ui.controlpanel.cropimage.state.CropState\nimport cn.netdiscovery.monica.ui.controlpanel.cropimage.state.cropData\nimport cn.netdiscovery.monica.ui.widget.image.gesture.detectMotionEventsAsList\nimport cn.netdiscovery.monica.ui.widget.image.gesture.detectTransformGestures\nimport cn.netdiscovery.monica.ui.controlpanel.cropimage.utils.ZoomLevel\nimport cn.netdiscovery.monica.ui.controlpanel.cropimage.utils.getNextZoomLevel\nimport cn.netdiscovery.monica.ui.controlpanel.cropimage.utils.update\nimport kotlinx.coroutines.launch\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.cropimage.CropModifier\n * @author: Tony Shen\n * @date: 2024/5/26 15:29\n * @version: V1.0 <描述当前版本功能>\n */\nfun Modifier.crop(\n    vararg keys: Any?,\n    cropState: CropState,\n    zoomOnDoubleTap: (ZoomLevel) -> Float = cropState.DefaultOnDoubleTap,\n    onDown: ((CropData) -> Unit)? = null,\n    onMove: ((CropData) -> Unit)? = null,\n    onUp: ((CropData) -> Unit)? = null,\n    onGestureStart: ((CropData) -> Unit)? = null,\n    onGesture: ((CropData) -> Unit)? = null,\n    onGestureEnd: ((CropData) -> Unit)? = null\n) = composed(\n\n    factory = {\n\n        LaunchedEffect(key1 = cropState){\n            cropState.init()\n        }\n\n        val coroutineScope = rememberCoroutineScope()\n\n        // Current Zoom level\n        var zoomLevel by remember { mutableStateOf(ZoomLevel.Min) }\n\n        val transformModifier = Modifier.pointerInput(*keys) {\n            detectTransformGestures(\n                consume = false,\n                onGestureStart = {\n                    onGestureStart?.invoke(cropState.cropData)\n                },\n                onGestureEnd = {\n                    coroutineScope.launch {\n                        cropState.onGestureEnd {\n                            onGestureEnd?.invoke(cropState.cropData)\n                        }\n                    }\n                },\n                onGesture = { centroid, pan, zoom, rotate, mainPointer, pointerList ->\n\n                    coroutineScope.launch {\n                        cropState.onGesture(\n                            centroid = centroid,\n                            panChange = pan,\n                            zoomChange = zoom,\n                            rotationChange = rotate,\n                            mainPointer = mainPointer,\n                            changes = pointerList\n                        )\n                    }\n                    onGesture?.invoke(cropState.cropData)\n                    mainPointer.consume()\n                }\n            )\n        }\n\n        val tapModifier = Modifier.pointerInput(*keys) {\n            detectTapGestures(\n                onDoubleTap = { offset: Offset ->\n                    coroutineScope.launch {\n                        zoomLevel = getNextZoomLevel(zoomLevel)\n                        val newZoom = zoomOnDoubleTap(zoomLevel)\n                        cropState.onDoubleTap(\n                            offset = offset,\n                            zoom = newZoom\n                        ) {\n                            onGestureEnd?.invoke(cropState.cropData)\n                        }\n                    }\n                }\n            )\n        }\n\n        val touchModifier = Modifier.pointerInput(*keys) {\n            detectMotionEventsAsList(\n                onDown = {\n                    coroutineScope.launch {\n                        cropState.onDown(it)\n                        onDown?.invoke(cropState.cropData)\n                    }\n                },\n                onMove = {\n                    coroutineScope.launch {\n                        cropState.onMove(it)\n                        onMove?.invoke(cropState.cropData)\n                    }\n                },\n                onUp = {\n                    coroutineScope.launch {\n                        cropState.onUp(it)\n                        onUp?.invoke(cropState.cropData)\n                    }\n                }\n            )\n        }\n\n        val graphicsModifier = Modifier.graphicsLayer {\n            this.update(cropState)\n        }\n\n        this.then(\n            clipToBounds()\n                .then(tapModifier)\n                .then(transformModifier)\n                .then(touchModifier)\n                .then(graphicsModifier)\n        )\n    },\n    inspectorInfo = debugInspectorInfo {\n        name = \"crop\"\n        // add name and value of each argument\n        properties[\"keys\"] = keys\n        properties[\"onDown\"] = onGestureStart\n        properties[\"onMove\"] = onGesture\n        properties[\"onUp\"] = onGestureEnd\n    }\n)\n\ninternal val CropState.DefaultOnDoubleTap: (ZoomLevel) -> Float\n    get() = { zoomLevel: ZoomLevel ->\n        when (zoomLevel) {\n            ZoomLevel.Min -> 1f\n            ZoomLevel.Mid -> 3f.coerceIn(zoomMin, zoomMax)\n            ZoomLevel.Max -> 5f.coerceAtLeast(zoomMax)\n        }\n    }\n"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/cropimage/CropViewModel.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.cropimage\n\nimport cn.netdiscovery.monica.config.KEY_CROP_FIRST\nimport cn.netdiscovery.monica.config.KEY_CROP_SECOND\nimport cn.netdiscovery.monica.rxcache.rxCache\nimport cn.netdiscovery.monica.utils.logger\nimport org.slf4j.Logger\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.cropimage.CropViewModel\n * @author: Tony Shen\n * @date: 2024/5/8 11:46\n * @version: V1.0 <描述当前版本功能>\n */\nclass CropViewModel {\n    private val logger: Logger = logger<CropViewModel>()\n\n    fun clearCropImageView() {\n        cropTypesIndex.value = 0\n        contentScalesIndex.value = 1\n\n        cropFlag1.set(false)\n        cropFlag2.set(false)\n        rxCache.remove(KEY_CROP_FIRST)\n        rxCache.remove(KEY_CROP_SECOND)\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/cropimage/ImageCropper.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.cropimage\n\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.ExperimentalAnimationApi\nimport androidx.compose.animation.animateColorAsState\nimport androidx.compose.animation.core.LinearEasing\nimport androidx.compose.animation.core.tween\nimport androidx.compose.animation.scaleIn\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clipToBounds\nimport androidx.compose.ui.geometry.Rect\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.FilterQuality\nimport androidx.compose.ui.graphics.ImageBitmap\nimport androidx.compose.ui.graphics.drawscope.DrawScope\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.platform.LocalLayoutDirection\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.IntSize\nimport cn.netdiscovery.monica.config.KEY_CROP\nimport cn.netdiscovery.monica.config.KEY_CROP_FIRST\nimport cn.netdiscovery.monica.config.KEY_CROP_SECOND\nimport cn.netdiscovery.monica.rxcache.rxCache\nimport cn.netdiscovery.monica.ui.controlpanel.cropimage.draw.DrawingOverlay\nimport cn.netdiscovery.monica.ui.controlpanel.cropimage.draw.ImageDrawCanvas\nimport cn.netdiscovery.monica.ui.controlpanel.cropimage.model.CropOutline\nimport cn.netdiscovery.monica.ui.controlpanel.cropimage.setting.CropDefaults\nimport cn.netdiscovery.monica.ui.controlpanel.cropimage.setting.CropProperties\nimport cn.netdiscovery.monica.ui.controlpanel.cropimage.setting.CropStyle\nimport cn.netdiscovery.monica.ui.controlpanel.cropimage.setting.CropType\nimport cn.netdiscovery.monica.ui.controlpanel.cropimage.state.DynamicCropState\nimport cn.netdiscovery.monica.ui.controlpanel.cropimage.state.rememberCropState\nimport cn.netdiscovery.monica.ui.widget.image.ImageWithConstraints\nimport cn.netdiscovery.monica.ui.widget.image.getScaledImageBitmap\nimport com.safframework.kotlin.coroutines.Default\nimport com.safframework.rxcache.domain.CacheStrategy\nimport com.safframework.rxcache.ext.get\nimport com.safframework.rxcache.ext.saveMemoryFunc\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.flow.*\nimport java.util.concurrent.atomic.AtomicBoolean\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.cropimage.ImageCropper\n * @author: Tony Shen\n * @date: 2024/5/26 12:00\n * @version: V1.0 <描述当前版本功能>\n */\nval cropFlag1:AtomicBoolean = AtomicBoolean(false)\nval cropFlag2:AtomicBoolean = AtomicBoolean(false)\n\n@Composable\nfun ImageCropper(\n    modifier: Modifier = Modifier,\n    imageBitmap: ImageBitmap,\n    contentDescription: String?,\n    cropStyle: CropStyle = CropDefaults.style(),\n    cropProperties: CropProperties,\n    filterQuality: FilterQuality = DrawScope.DefaultFilterQuality,\n    crop: Boolean = false,\n    backgroundColor: Color = Color.Black,\n    onCropStart: () -> Unit,\n    onCropSuccess: (ImageBitmap) -> Unit,\n    onDrawGrid: (DrawScope.(rect: Rect, strokeWidth: Float, color: Color) -> Unit)? = null,\n) {\n\n    ImageWithConstraints(\n        modifier = modifier.clipToBounds(),\n        contentScale = cropProperties.contentScale,\n        contentDescription = contentDescription,\n        filterQuality = filterQuality,\n        imageBitmap = imageBitmap,\n        drawImage = false\n    ) {\n\n        // No crop operation is applied by ScalableImage so rect points to bounds of original\n        // bitmap\n        val scaledImageBitmap = getScaledImageBitmap(\n            imageWidth = imageWidth,\n            imageHeight = imageHeight,\n            rect = rect,\n            bitmap = imageBitmap,\n            contentScale = cropProperties.contentScale,\n        )\n\n        // Container Dimensions\n        val containerWidthPx = constraints.maxWidth\n        val containerHeightPx = constraints.maxHeight\n\n        val containerWidth: Dp\n        val containerHeight: Dp\n\n        // Bitmap Dimensions\n        val bitmapWidth = scaledImageBitmap.width\n        val bitmapHeight = scaledImageBitmap.height\n\n        // Dimensions of Composable that displays Bitmap\n        val imageWidthPx: Int\n        val imageHeightPx: Int\n\n        with(LocalDensity.current) {\n            imageWidthPx = imageWidth.roundToPx()\n            imageHeightPx = imageHeight.roundToPx()\n            containerWidth = containerWidthPx.toDp()\n            containerHeight = containerHeightPx.toDp()\n        }\n\n        val cropType = cropProperties.cropType\n        val contentScale = cropProperties.contentScale\n        val fixedAspectRatio = cropProperties.fixedAspectRatio\n        val cropOutline = cropProperties.cropOutlineProperty.cropOutline\n\n        // these keys are for resetting cropper when image width/height, contentScale or\n        // overlay aspect ratio changes\n        val resetKeys =\n            getResetKeys(\n                scaledImageBitmap,\n                imageWidthPx,\n                imageHeightPx,\n                contentScale,\n                cropType,\n                fixedAspectRatio\n            )\n\n        val cropState = rememberCropState(\n            imageSize = IntSize(bitmapWidth, bitmapHeight),\n            containerSize = IntSize(containerWidthPx, containerHeightPx),\n            drawAreaSize = IntSize(imageWidthPx, imageHeightPx),\n            cropProperties = cropProperties,\n            keys = resetKeys\n        )\n\n        val isHandleTouched by remember(cropState) {\n            derivedStateOf {\n                cropState is DynamicCropState && handlesTouched(cropState.touchRegion)\n            }\n        }\n\n        val pressedStateColor = remember(cropStyle.backgroundColor){\n            cropStyle.backgroundColor\n                .copy(cropStyle.backgroundColor.alpha * .7f)\n        }\n\n        val transparentColor by animateColorAsState(\n            animationSpec = tween(300, easing = LinearEasing),\n            targetValue = if (isHandleTouched) pressedStateColor else cropStyle.backgroundColor\n        )\n\n        if (!cropFlag1.get()) {\n            rxCache.saveMemoryFunc(KEY_CROP_FIRST) {\n                cropFlag1.set(true)\n                cropState.cropRect\n            }\n        }\n\n        val cachedRect = rxCache.get<Rect>(KEY_CROP_SECOND, CacheStrategy.MEMORY)?.data?:\n        rxCache.get<Rect>(KEY_CROP_FIRST, CacheStrategy.MEMORY)?.data\n\n        if (cachedRect != cropState.cropRect) {\n            if (!cropFlag2.get()) {\n                rxCache.saveMemoryFunc(KEY_CROP_SECOND) {\n                    cropFlag2.set(true)\n                    cropState.cropRect\n                }\n            } else {\n                rxCache.saveMemory(KEY_CROP, cropState.cropRect)\n            }\n        }\n\n        // Crops image when user invokes crop operation\n        Crop(\n            crop,\n            scaledImageBitmap,\n            cropState.cropRect,\n            cropOutline,\n            onCropStart,\n            onCropSuccess,\n            cropProperties.requiredSize\n        )\n\n        val imageModifier = Modifier\n            .size(containerWidth, containerHeight)\n            .crop(\n                keys = resetKeys,\n                cropState = cropState\n            )\n\n        LaunchedEffect(key1 = cropProperties) {\n            cropState.updateProperties(cropProperties)\n        }\n\n        /// Create a MutableTransitionState<Boolean> for the AnimatedVisibility.\n        var visible by remember { mutableStateOf(false) }\n\n        LaunchedEffect(Unit) {\n            delay(100)\n            visible = true\n        }\n\n        ImageCropper(\n            modifier = imageModifier,\n            visible = visible,\n            imageBitmap = imageBitmap,\n            containerWidth = containerWidth,\n            containerHeight = containerHeight,\n            imageWidthPx = imageWidthPx,\n            imageHeightPx = imageHeightPx,\n            handleSize = cropProperties.handleSize,\n            overlayRect = cropState.overlayRect,\n            cropType = cropType,\n            cropOutline = cropOutline,\n            cropStyle = cropStyle,\n            transparentColor = transparentColor,\n            backgroundColor = backgroundColor,\n            onDrawGrid = onDrawGrid,\n        )\n    }\n}\n\n@OptIn(ExperimentalAnimationApi::class)\n@Composable\nprivate fun ImageCropper(\n    modifier: Modifier,\n    visible: Boolean,\n    imageBitmap: ImageBitmap,\n    containerWidth: Dp,\n    containerHeight: Dp,\n    imageWidthPx: Int,\n    imageHeightPx: Int,\n    handleSize: Float,\n    cropType: CropType,\n    cropOutline: CropOutline,\n    cropStyle: CropStyle,\n    overlayRect: Rect,\n    transparentColor: Color,\n    backgroundColor: Color,\n    onDrawGrid: (DrawScope.(rect: Rect, strokeWidth: Float, color: Color) -> Unit)?,\n) {\n    Box(\n        modifier = Modifier\n            .fillMaxSize()\n            .background(backgroundColor)\n    ) {\n\n        AnimatedVisibility(\n            visible = visible,\n            enter = scaleIn(tween(500))\n        ) {\n\n            ImageCropperImpl(\n                modifier = modifier,\n                imageBitmap = imageBitmap,\n                containerWidth = containerWidth,\n                containerHeight = containerHeight,\n                imageWidthPx = imageWidthPx,\n                imageHeightPx = imageHeightPx,\n                cropType = cropType,\n                cropOutline = cropOutline,\n                handleSize = handleSize,\n                cropStyle = cropStyle,\n                rectOverlay = overlayRect,\n                transparentColor = transparentColor,\n                onDrawGrid = onDrawGrid,\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun ImageCropperImpl(\n    modifier: Modifier,\n    imageBitmap: ImageBitmap,\n    containerWidth: Dp,\n    containerHeight: Dp,\n    imageWidthPx: Int,\n    imageHeightPx: Int,\n    cropType: CropType,\n    cropOutline: CropOutline,\n    handleSize: Float,\n    cropStyle: CropStyle,\n    transparentColor: Color,\n    rectOverlay: Rect,\n    onDrawGrid: (DrawScope.(rect: Rect, strokeWidth: Float, color: Color) -> Unit)?,\n) {\n\n    Box(contentAlignment = Alignment.Center) {\n\n        // Draw Image\n        ImageDrawCanvas(\n            modifier = modifier,\n            imageBitmap = imageBitmap,\n            imageWidth = imageWidthPx,\n            imageHeight = imageHeightPx\n        )\n\n        val drawOverlay = cropStyle.drawOverlay\n\n        val drawGrid = cropStyle.drawGrid\n        val overlayColor = cropStyle.overlayColor\n        val handleColor = cropStyle.handleColor\n        val drawHandles = cropType == CropType.Dynamic\n        val strokeWidth = cropStyle.strokeWidth\n\n        DrawingOverlay(\n            modifier = Modifier.size(containerWidth, containerHeight),\n            drawOverlay = drawOverlay,\n            rect = rectOverlay,\n            cropOutline = cropOutline,\n            drawGrid = drawGrid,\n            overlayColor = overlayColor,\n            handleColor = handleColor,\n            strokeWidth = strokeWidth,\n            drawHandles = drawHandles,\n            handleSize = handleSize,\n            transparentColor = transparentColor,\n            onDrawGrid = onDrawGrid,\n        )\n    }\n}\n\n@Composable\nprivate fun Crop(\n    crop: Boolean,\n    scaledImageBitmap: ImageBitmap,\n    rect: Rect,\n    cropOutline: CropOutline,\n    onCropStart: () -> Unit,\n    onCropSuccess: (ImageBitmap) -> Unit,\n    requiredSize: IntSize?,\n) {\n    val cropRect = rxCache.get<Rect>(KEY_CROP, CacheStrategy.MEMORY)?.data ?: rect\n\n    val density = LocalDensity.current\n    val layoutDirection = LocalLayoutDirection.current\n\n    // Crop Agent is responsible for cropping image\n    val cropAgent = remember { CropAgent() }\n\n    LaunchedEffect(crop) {\n        if (crop) {\n            flow {\n                val croppedImageBitmap = cropAgent.crop(\n                    scaledImageBitmap,\n                    cropRect,\n                    cropOutline,\n                    layoutDirection,\n                    density\n                )\n                if (requiredSize != null) {\n                    emit(\n                        cropAgent.resize(\n                            croppedImageBitmap,\n                            requiredSize.width,\n                            requiredSize.height,\n                        )\n                    )\n                } else {\n                    emit(croppedImageBitmap)\n                }\n            }\n                .flowOn(Default)\n                .onStart {\n                    onCropStart()\n                    delay(400)\n                }\n                .onEach {\n                    onCropSuccess(it)\n                }\n                .launchIn(this)\n        }\n    }\n}\n\n@Composable\nprivate fun getResetKeys(\n    scaledImageBitmap: ImageBitmap,\n    imageWidthPx: Int,\n    imageHeightPx: Int,\n    contentScale: ContentScale,\n    cropType: CropType,\n    fixedAspectRatio: Boolean,\n) = remember(\n    scaledImageBitmap,\n    imageWidthPx,\n    imageHeightPx,\n    contentScale,\n    cropType,\n    fixedAspectRatio,\n) {\n    arrayOf(\n        scaledImageBitmap,\n        imageWidthPx,\n        imageHeightPx,\n        contentScale,\n        cropType,\n        fixedAspectRatio,\n    )\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/cropimage/TouchRegion.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.cropimage\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.cropimage.TouchRegion\n * @author: Tony Shen\n * @date: 2024/5/26 12:17\n * @version: V1.0 <描述当前版本功能>\n */\nenum class TouchRegion {\n    TopLeft, TopRight, BottomLeft, BottomRight, Inside, None\n}\n\nfun handlesTouched(touchRegion: TouchRegion) = touchRegion == TouchRegion.TopLeft ||\n        touchRegion == TouchRegion.TopRight ||\n        touchRegion == TouchRegion.BottomLeft ||\n        touchRegion == TouchRegion.BottomRight"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/cropimage/draw/ImageDrawCanvas.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.cropimage.draw\n\nimport androidx.compose.foundation.Canvas\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.ImageBitmap\nimport androidx.compose.ui.unit.IntOffset\nimport androidx.compose.ui.unit.IntSize\nimport kotlin.math.roundToInt\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.cropimage.draw.ImageDrawCanvas\n * @author: Tony Shen\n * @date: 2024/5/26 15:37\n * @version: V1.0 <描述当前版本功能>\n */\n@Composable\ninternal fun ImageDrawCanvas(\n    modifier: Modifier,\n    imageBitmap: ImageBitmap,\n    imageWidth: Int,\n    imageHeight: Int\n) {\n    Canvas(modifier = modifier) {\n\n        val canvasWidth = size.width.roundToInt()\n        val canvasHeight = size.height.roundToInt()\n\n        drawImage(\n            image = imageBitmap,\n            srcSize = IntSize(imageBitmap.width, imageBitmap.height),\n            dstSize = IntSize(imageWidth, imageHeight),\n            dstOffset = IntOffset(\n                x = (canvasWidth - imageWidth) / 2,\n                y = (canvasHeight - imageHeight) / 2\n            )\n        )\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/cropimage/draw/Overlay.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.cropimage.draw\n\nimport androidx.compose.foundation.Canvas\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.geometry.Rect\nimport androidx.compose.ui.graphics.*\nimport androidx.compose.ui.graphics.drawscope.DrawScope\nimport androidx.compose.ui.graphics.drawscope.Stroke\nimport androidx.compose.ui.graphics.drawscope.translate\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.platform.LocalLayoutDirection\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.IntSize\nimport androidx.compose.ui.unit.LayoutDirection\nimport cn.netdiscovery.monica.ui.controlpanel.cropimage.model.CropImageMask\nimport cn.netdiscovery.monica.ui.controlpanel.cropimage.model.CropOutline\nimport cn.netdiscovery.monica.ui.controlpanel.cropimage.model.CropPath\nimport cn.netdiscovery.monica.ui.controlpanel.cropimage.model.CropShape\nimport cn.netdiscovery.monica.ui.controlpanel.cropimage.utils.drawGrid\nimport cn.netdiscovery.monica.ui.controlpanel.cropimage.utils.scaleAndTranslatePath\nimport cn.netdiscovery.monica.utils.extensions.drawWithLayer\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.cropimage.draw.Overlay\n * @author: Tony Shen\n * @date: 2024/5/26 15:38\n * @version: V1.0 <描述当前版本功能>\n */\n@Composable\ninternal fun DrawingOverlay(\n    modifier: Modifier,\n    drawOverlay: Boolean,\n    rect: Rect,\n    cropOutline: CropOutline,\n    drawGrid: Boolean,\n    transparentColor: Color,\n    overlayColor: Color,\n    handleColor: Color,\n    strokeWidth: Dp,\n    drawHandles: Boolean,\n    handleSize: Float,\n    onDrawGrid: (DrawScope.(rect: Rect, strokeWidth: Float, color: Color) -> Unit)?\n) {\n    val density = LocalDensity.current\n    val layoutDirection: LayoutDirection = LocalLayoutDirection.current\n\n    val strokeWidthPx = LocalDensity.current.run { strokeWidth.toPx() }\n\n    val pathHandles = remember {\n        Path()\n    }\n\n    when (cropOutline) {\n        is CropShape -> {\n\n            val outline = remember(rect, cropOutline) {\n                cropOutline.shape.createOutline(rect.size, layoutDirection, density)\n            }\n\n            DrawingOverlayImpl(\n                modifier = modifier,\n                drawOverlay = drawOverlay,\n                rect = rect,\n                drawGrid = drawGrid,\n                transparentColor = transparentColor,\n                overlayColor = overlayColor,\n                handleColor = handleColor,\n                strokeWidth = strokeWidthPx,\n                drawHandles = drawHandles,\n                handleSize = handleSize,\n                pathHandles = pathHandles,\n                outline = outline,\n                onDrawGrid = onDrawGrid,\n            )\n        }\n        is CropPath -> {\n            val path = remember(rect, cropOutline) {\n                Path().apply {\n                    addPath(cropOutline.path)\n                    scaleAndTranslatePath(rect.width, rect.height)\n                }\n            }\n\n\n            DrawingOverlayImpl(\n                modifier = modifier,\n                drawOverlay = drawOverlay,\n                rect = rect,\n                drawGrid = drawGrid,\n                transparentColor = transparentColor,\n                overlayColor = overlayColor,\n                handleColor = handleColor,\n                strokeWidth = strokeWidthPx,\n                drawHandles = drawHandles,\n                handleSize = handleSize,\n                pathHandles = pathHandles,\n                path = path,\n                onDrawGrid = onDrawGrid,\n            )\n        }\n        is CropImageMask -> {\n            val imageBitmap = cropOutline.image\n\n            DrawingOverlayImpl(\n                modifier = modifier,\n                drawOverlay = drawOverlay,\n                rect = rect,\n                drawGrid = drawGrid,\n                transparentColor = transparentColor,\n                overlayColor = overlayColor,\n                handleColor = handleColor,\n                strokeWidth = strokeWidthPx,\n                drawHandles = drawHandles,\n                handleSize = handleSize,\n                pathHandles = pathHandles,\n                image = imageBitmap,\n                onDrawGrid = onDrawGrid,\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun DrawingOverlayImpl(\n    modifier: Modifier,\n    drawOverlay: Boolean,\n    rect: Rect,\n    drawGrid: Boolean,\n    transparentColor: Color,\n    overlayColor: Color,\n    handleColor: Color,\n    strokeWidth: Float,\n    drawHandles: Boolean,\n    handleSize: Float,\n    pathHandles: Path,\n    outline: Outline,\n    onDrawGrid: (DrawScope.(rect: Rect, strokeWidth: Float, color: Color) -> Unit)?,\n) {\n    Canvas(modifier = modifier) {\n        drawOverlay(\n            drawOverlay,\n            rect,\n            drawGrid,\n            transparentColor,\n            overlayColor,\n            handleColor,\n            strokeWidth,\n            drawHandles,\n            handleSize,\n            pathHandles,\n            onDrawGrid,\n        ) {\n            drawCropOutline(outline = outline)\n        }\n    }\n}\n\n@Composable\nprivate fun DrawingOverlayImpl(\n    modifier: Modifier,\n    drawOverlay: Boolean,\n    rect: Rect,\n    drawGrid: Boolean,\n    transparentColor: Color,\n    overlayColor: Color,\n    handleColor: Color,\n    strokeWidth: Float,\n    drawHandles: Boolean,\n    handleSize: Float,\n    pathHandles: Path,\n    path: Path,\n    onDrawGrid: (DrawScope.(rect: Rect, strokeWidth: Float, color: Color) -> Unit)?,\n) {\n    Canvas(modifier = modifier) {\n        drawOverlay(\n            drawOverlay,\n            rect,\n            drawGrid,\n            transparentColor,\n            overlayColor,\n            handleColor,\n            strokeWidth,\n            drawHandles,\n            handleSize,\n            pathHandles,\n            onDrawGrid,\n        ) {\n            drawCropPath(path)\n        }\n    }\n}\n\n@Composable\nprivate fun DrawingOverlayImpl(\n    modifier: Modifier,\n    drawOverlay: Boolean,\n    rect: Rect,\n    drawGrid: Boolean,\n    transparentColor: Color,\n    overlayColor: Color,\n    handleColor: Color,\n    strokeWidth: Float,\n    drawHandles: Boolean,\n    handleSize: Float,\n    pathHandles: Path,\n    image: ImageBitmap,\n    onDrawGrid: (DrawScope.(rect: Rect, strokeWidth: Float, color: Color) -> Unit)?,\n) {\n    Canvas(modifier = modifier) {\n        drawOverlay(\n            drawOverlay,\n            rect,\n            drawGrid,\n            transparentColor,\n            overlayColor,\n            handleColor,\n            strokeWidth,\n            drawHandles,\n            handleSize,\n            pathHandles,\n            onDrawGrid,\n        ) {\n            drawCropImage(rect, image)\n        }\n    }\n}\n\nprivate fun DrawScope.drawOverlay(\n    drawOverlay: Boolean,\n    rect: Rect,\n    drawGrid: Boolean,\n    transparentColor: Color,\n    overlayColor: Color,\n    handleColor: Color,\n    strokeWidth: Float,\n    drawHandles: Boolean,\n    handleSize: Float,\n    pathHandles: Path,\n    onDrawGrid: (DrawScope.(rect: Rect, strokeWidth: Float, color: Color) -> Unit)?,\n    drawBlock: DrawScope.() -> Unit,\n) {\n    drawWithLayer {\n\n        // Destination\n        drawRect(transparentColor)\n\n        // Source\n        translate(left = rect.left, top = rect.top) {\n            drawBlock()\n        }\n\n        if (drawGrid) {\n            if (onDrawGrid != null) {\n                onDrawGrid(rect, strokeWidth, overlayColor)\n            } else {\n                drawGrid(\n                    rect = rect,\n                    strokeWidth = strokeWidth,\n                    color = overlayColor,\n                )\n            }\n        }\n    }\n\n    if (drawOverlay) {\n        drawRect(\n            topLeft = rect.topLeft,\n            size = rect.size,\n            color = overlayColor,\n            style = Stroke(width = strokeWidth)\n        )\n\n        if (drawHandles) {\n            pathHandles.apply {\n                reset()\n                updateHandlePath(rect, handleSize)\n            }\n\n            drawPath(\n                path = pathHandles,\n                color = handleColor,\n                style = Stroke(\n                    width = strokeWidth * 2,\n                    cap = StrokeCap.Round,\n                    join = StrokeJoin.Round\n                )\n            )\n        }\n    }\n}\n\nprivate fun DrawScope.drawCropImage(\n    rect: Rect,\n    imageBitmap: ImageBitmap,\n    blendMode: BlendMode = BlendMode.DstOut\n) {\n    drawImage(\n        image = imageBitmap,\n        dstSize = IntSize(rect.size.width.toInt(), rect.size.height.toInt()),\n        blendMode = blendMode\n    )\n}\n\nprivate fun DrawScope.drawCropOutline(\n    outline: Outline,\n    blendMode: BlendMode = BlendMode.SrcOut\n) {\n    drawOutline(\n        outline = outline,\n        color = Color.Transparent,\n        blendMode = blendMode\n    )\n}\n\nprivate fun DrawScope.drawCropPath(\n    path: Path,\n    blendMode: BlendMode = BlendMode.SrcOut\n) {\n    drawPath(\n        path = path,\n        color = Color.Transparent,\n        blendMode = blendMode\n    )\n}\n\nprivate fun Path.updateHandlePath(\n    rect: Rect,\n    handleSize: Float\n) {\n    if (rect != Rect.Zero) {\n        // Top left lines\n        moveTo(rect.topLeft.x, rect.topLeft.y + handleSize)\n        lineTo(rect.topLeft.x, rect.topLeft.y)\n        lineTo(rect.topLeft.x + handleSize, rect.topLeft.y)\n\n        // Top right lines\n        moveTo(rect.topRight.x - handleSize, rect.topRight.y)\n        lineTo(rect.topRight.x, rect.topRight.y)\n        lineTo(rect.topRight.x, rect.topRight.y + handleSize)\n\n        // Bottom right lines\n        moveTo(rect.bottomRight.x, rect.bottomRight.y - handleSize)\n        lineTo(rect.bottomRight.x, rect.bottomRight.y)\n        lineTo(rect.bottomRight.x - handleSize, rect.bottomRight.y)\n\n        // Bottom left lines\n        moveTo(rect.bottomLeft.x + handleSize, rect.bottomLeft.y)\n        lineTo(rect.bottomLeft.x, rect.bottomLeft.y)\n        lineTo(rect.bottomLeft.x, rect.bottomLeft.y - handleSize)\n    }\n}\n"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/cropimage/model/CropAspectRatio.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.cropimage.model\n\nimport androidx.compose.runtime.Immutable\nimport androidx.compose.ui.graphics.Shape\nimport cn.netdiscovery.monica.ui.controlpanel.cropimage.utils.createRectShape\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.cropimage.model.CropAspectRatio\n * @author: Tony Shen\n * @date: 2024/6/10 17:29\n * @version: V1.0 <描述当前版本功能>\n */\nval aspectRatios = listOf(\n    CropAspectRatio(\n        title = \"Original\",\n        shape = createRectShape(AspectRatio.Original),\n        aspectRatio = AspectRatio.Original\n    ),\n    CropAspectRatio(\n        title = \"9:16\",\n        shape = createRectShape(AspectRatio(9 / 16f)),\n        aspectRatio = AspectRatio(9 / 16f)\n    ),\n    CropAspectRatio(\n        title = \"2:3\",\n        shape = createRectShape(AspectRatio(2 / 3f)),\n        aspectRatio = AspectRatio(2 / 3f)\n    ),\n    CropAspectRatio(\n        title = \"1:1\",\n        shape = createRectShape(AspectRatio(1 / 1f)),\n        aspectRatio = AspectRatio(1 / 1f)\n    ),\n    CropAspectRatio(\n        title = \"16:9\",\n        shape = createRectShape(AspectRatio(16 / 9f)),\n        aspectRatio = AspectRatio(16 / 9f)\n    ),\n    CropAspectRatio(\n        title = \"1.91:1\",\n        shape = createRectShape(AspectRatio(1.91f / 1f)),\n        aspectRatio = AspectRatio(1.91f / 1f)\n    ),\n    CropAspectRatio(\n        title = \"3:2\",\n        shape = createRectShape(AspectRatio(3 / 2f)),\n        aspectRatio = AspectRatio(3 / 2f)\n    ),\n    CropAspectRatio(\n        title = \"3:4\",\n        shape = createRectShape(AspectRatio(3 / 4f)),\n        aspectRatio = AspectRatio(3 / 4f)\n    ),\n    CropAspectRatio(\n        title = \"3:5\",\n        shape = createRectShape(AspectRatio(3 / 5f)),\n        aspectRatio = AspectRatio(3 / 5f)\n    )\n)\n\n@Immutable\ndata class CropAspectRatio(\n    val title: String,\n    val shape: Shape,\n    val aspectRatio: AspectRatio = AspectRatio.Original,\n    val icons: List<Int> = listOf()\n)\n\n/**\n * Value class for containing aspect ratio\n * and [AspectRatio.Original] for comparing\n */\n@Immutable\ndata class AspectRatio(val value: Float) {\n    companion object {\n        val Original = AspectRatio(-1f)\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/cropimage/model/CropFrame.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.cropimage.model\n\nimport androidx.compose.runtime.Immutable\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.cropimage.model.CropFrame\n * @author: Tony Shen\n * @date: 2024/5/26 15:23\n * @version: V1.0 <描述当前版本功能>\n */\n@Immutable\ndata class CropFrame(\n    val outlineType: OutlineType,\n    val editable: Boolean = false,\n    val cropOutlineContainer: CropOutlineContainer<out CropOutline>\n) {\n    var selectedIndex: Int\n        get() = cropOutlineContainer.selectedIndex\n        set(value) {\n            cropOutlineContainer.selectedIndex = value\n        }\n\n    val outlines: List<CropOutline>\n        get() = cropOutlineContainer.outlines\n\n    val outlineCount: Int\n        get() = cropOutlineContainer.size\n\n    fun addOutline(outline: CropOutline): CropFrame {\n        outlines.toMutableList().add(outline)\n        return this\n    }\n}\n\n@Suppress(\"UNCHECKED_CAST\")\nfun getOutlineContainer(\n    outlineType: OutlineType,\n    index: Int,\n    outlines: List<CropOutline>\n): CropOutlineContainer<out CropOutline> {\n    return when (outlineType) {\n        OutlineType.RoundedRect -> {\n            RoundedRectOutlineContainer(\n                selectedIndex = index,\n                outlines = outlines as List<RoundedCornerCropShape>\n            )\n        }\n        OutlineType.CutCorner -> {\n            CutCornerRectOutlineContainer(\n                selectedIndex = index,\n                outlines = outlines as List<CutCornerCropShape>\n            )\n        }\n\n        OutlineType.Oval -> {\n            OvalOutlineContainer(\n                selectedIndex = index,\n                outlines = outlines as List<OvalCropShape>\n            )\n        }\n\n        OutlineType.Polygon -> {\n            PolygonOutlineContainer(\n                selectedIndex = index,\n                outlines = outlines as List<PolygonCropShape>\n            )\n        }\n\n        OutlineType.Diamond -> {\n            DiamondOutlineContainer(\n                selectedIndex = index,\n                outlines = outlines as List<DiamondShape>\n            )\n        }\n\n        OutlineType.Ticket -> {\n            TicketOutlineContainer(\n                selectedIndex = index,\n                outlines = outlines as List<TicketShape>\n            )\n        }\n\n        OutlineType.Custom -> {\n            CustomOutlineContainer(\n                selectedIndex = index,\n                outlines = outlines as List<CustomPathOutline>\n            )\n        }\n\n        OutlineType.ImageMask -> {\n            ImageMaskOutlineContainer(\n                selectedIndex = index,\n                outlines = outlines as List<ImageMaskOutline>\n            )\n        }\n        else -> {\n            RectOutlineContainer(\n                selectedIndex = index,\n                outlines = outlines as List<RectCropShape>\n            )\n        }\n    }\n}\n\n\nenum class OutlineType {\n    Rect, RoundedRect, CutCorner, Oval, Polygon, Parallelogram, Diamond, Ticket, Custom, ImageMask\n}\n"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/cropimage/model/CropOutline.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.cropimage.model\n\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.shape.CutCornerShape\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.runtime.Immutable\nimport androidx.compose.ui.graphics.ImageBitmap\nimport androidx.compose.ui.graphics.Path\nimport androidx.compose.ui.graphics.RectangleShape\nimport androidx.compose.ui.graphics.Shape\nimport cn.netdiscovery.monica.ui.controlpanel.cropimage.utils.Diamond\nimport cn.netdiscovery.monica.ui.controlpanel.cropimage.utils.Parallelogram\nimport cn.netdiscovery.monica.ui.controlpanel.cropimage.utils.Ticket\nimport cn.netdiscovery.monica.ui.controlpanel.cropimage.utils.createPolygonShape\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.cropimage.model.CropOutline\n * @author: Tony Shen\n * @date: 2024/5/26 12:22\n * @version: V1.0 <描述当前版本功能>\n */\ninterface CropOutline {\n    val id: Int\n    val title: String\n}\n\n/**\n * Crop outline that contains a [Shape] like [RectangleShape] to draw frame for cropping\n */\ninterface CropShape : CropOutline {\n    val shape: Shape\n}\n\n/**\n * Crop outline that contains a [Path] to draw frame for cropping\n */\ninterface CropPath : CropOutline {\n    val path: Path\n}\n\n/**\n * Crop outline that contains a [ImageBitmap]  to draw frame for cropping. And blend modes\n * to draw\n */\ninterface CropImageMask : CropOutline {\n    val image: ImageBitmap\n}\n\n/**\n * Wrapper class that implements [CropOutline] and is a shape\n * wrapper that contains [RectangleShape]\n */\n@Immutable\ndata class RectCropShape(\n    override val id: Int,\n    override val title: String,\n) : CropShape {\n    override val shape: Shape = RectangleShape\n}\n\n/**\n * Wrapper class that implements [CropOutline] and is a shape\n * wrapper that contains [RoundedCornerShape]\n */\n@Immutable\ndata class RoundedCornerCropShape(\n    override val id: Int,\n    override val title: String,\n    val cornerRadius: CornerRadiusProperties = CornerRadiusProperties(),\n    override val shape: RoundedCornerShape = RoundedCornerShape(\n        topStartPercent = cornerRadius.topStartPercent,\n        topEndPercent = cornerRadius.topEndPercent,\n        bottomEndPercent = cornerRadius.bottomEndPercent,\n        bottomStartPercent = cornerRadius.bottomStartPercent\n    )\n) : CropShape\n\n/**\n * Wrapper class that implements [CropOutline] and is a shape\n * wrapper that contains [CutCornerShape]\n */\n@Immutable\ndata class CutCornerCropShape(\n    override val id: Int,\n    override val title: String,\n    val cornerRadius: CornerRadiusProperties = CornerRadiusProperties(),\n    override val shape: CutCornerShape = CutCornerShape(\n        topStartPercent = cornerRadius.topStartPercent,\n        topEndPercent = cornerRadius.topEndPercent,\n        bottomEndPercent = cornerRadius.bottomEndPercent,\n        bottomStartPercent = cornerRadius.bottomStartPercent\n    )\n) : CropShape\n\n/**\n * Wrapper class that implements [CropOutline] and is a shape\n * wrapper that contains [CircleShape]\n */\n@Immutable\ndata class OvalCropShape(\n    override val id: Int,\n    override val title: String,\n    val ovalProperties: OvalProperties = OvalProperties(),\n    override val shape: Shape = CircleShape\n) : CropShape\n\n\n/**\n * Wrapper class that implements [CropOutline] and is a shape\n * wrapper that contains [CircleShape]\n */\n@Immutable\ndata class PolygonCropShape(\n    override val id: Int,\n    override val title: String,\n    val polygonProperties: PolygonProperties = PolygonProperties(),\n    override val shape: Shape = createPolygonShape(polygonProperties.sides, polygonProperties.angle)\n) : CropShape\n\n@Immutable\ndata class ParallelogramShape(\n    override val id: Int,\n    override val title: String,\n    override val shape: Shape = Parallelogram(70f)\n): CropShape\n\n@Immutable\ndata class DiamondShape(\n    override val id: Int,\n    override val title: String,\n    override val shape: Shape = Diamond()\n): CropShape\n\n@Immutable\ndata class TicketShape(\n    override val id: Int,\n    override val title: String,\n    override val shape: Shape = Ticket()\n): CropShape\n\n/**\n * Wrapper class that implements [CropOutline] and is a [Path] wrapper to crop using drawable\n * files converted fom svg or Vector Drawable to [Path]\n */\n@Immutable\ndata class CustomPathOutline(\n    override val id: Int,\n    override val title: String,\n    override val path: Path\n) : CropPath\n\n/**\n * Wrapper class that implements [CropOutline] and is a [ImageBitmap] wrapper to crop\n * using a reference png and blend modes to crop\n */\n@Immutable\ndata class ImageMaskOutline(\n    override val id: Int,\n    override val title: String,\n    override val image: ImageBitmap,\n) : CropImageMask"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/cropimage/model/CropOutlineContainer.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.cropimage.model\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.cropimage.model.CropOutlineContainer\n * @author: Tony Shen\n * @date: 2024/5/26 15:24\n * @version: V1.0 <描述当前版本功能>\n */\ninterface CropOutlineContainer<O : CropOutline> {\n    var selectedIndex: Int\n    val outlines: List<O>\n    val selectedItem: O\n        get() = outlines[selectedIndex]\n    val size: Int\n        get() = outlines.size\n}\n\n/**\n * Container for [RectCropShape]\n */\ndata class RectOutlineContainer(\n    override var selectedIndex: Int = 0,\n    override val outlines: List<RectCropShape>\n) : CropOutlineContainer<RectCropShape>\n\n/**\n * Container for [RoundedCornerCropShape]s\n */\ndata class RoundedRectOutlineContainer(\n    override var selectedIndex: Int = 0,\n    override val outlines: List<RoundedCornerCropShape>\n) : CropOutlineContainer<RoundedCornerCropShape>\n\n/**\n * Container for [CutCornerCropShape]s\n */\ndata class CutCornerRectOutlineContainer(\n    override var selectedIndex: Int = 0,\n    override val outlines: List<CutCornerCropShape>\n) : CropOutlineContainer<CutCornerCropShape>\n\n/**\n * Container for [OvalCropShape]s\n */\ndata class OvalOutlineContainer(\n    override var selectedIndex: Int = 0,\n    override val outlines: List<OvalCropShape>\n) : CropOutlineContainer<OvalCropShape>\n\n\n/**\n * Container for [PolygonCropShape]s\n */\ndata class PolygonOutlineContainer(\n    override var selectedIndex: Int = 0,\n    override val outlines: List<PolygonCropShape>\n) : CropOutlineContainer<PolygonCropShape>\n\n/**\n * Container for [ParallelogramShape]s\n */\ndata class ParallelogramOutlineContainer(\n    override var selectedIndex: Int = 0,\n    override val outlines: List<ParallelogramShape>\n) : CropOutlineContainer<ParallelogramShape>\n\n\n/**\n * Container for [DiamondShape]s\n */\ndata class DiamondOutlineContainer(\n    override var selectedIndex: Int = 0,\n    override val outlines: List<DiamondShape>\n) : CropOutlineContainer<DiamondShape>\n\n\n/**\n * Container for [TicketShape]s\n */\ndata class TicketOutlineContainer(\n    override var selectedIndex: Int = 0,\n    override val outlines: List<TicketShape>\n) : CropOutlineContainer<TicketShape>\n\n/**\n * Container for [CustomPathOutline]s\n */\ndata class CustomOutlineContainer(\n    override var selectedIndex: Int = 0,\n    override val outlines: List<CustomPathOutline>\n) : CropOutlineContainer<CustomPathOutline>\n\n/**\n * Container for [ImageMaskOutline]s\n */\ndata class ImageMaskOutlineContainer(\n    override var selectedIndex: Int = 0,\n    override val outlines: List<ImageMaskOutline>\n) : CropOutlineContainer<ImageMaskOutline>"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/cropimage/model/CropOutlineProperties.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.cropimage.model\n\nimport androidx.compose.runtime.Immutable\nimport androidx.compose.ui.geometry.Offset\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.cropimage.model.CropOutlineProperties\n * @author: Tony Shen\n * @date: 2024/5/26 12:23\n * @version: V1.0 <描述当前版本功能>\n */\n@Immutable\ndata class CornerRadiusProperties(\n    val topStartPercent: Int = 20,\n    val topEndPercent: Int = 20,\n    val bottomStartPercent: Int = 20,\n    val bottomEndPercent: Int = 20\n)\n\n@Immutable\ndata class PolygonProperties(\n    val sides: Int = 6,\n    val angle: Float = 0f,\n    val offset: Offset = Offset.Zero\n)\n\n@Immutable\ndata class OvalProperties(\n    val startAngle: Float = 0f,\n    val sweepAngle: Float = 360f,\n    val offset: Offset = Offset.Zero\n)"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/cropimage/setting/CropDefaults.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.cropimage.setting\n\nimport androidx.compose.runtime.Immutable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.IntSize\nimport androidx.compose.ui.unit.dp\nimport cn.netdiscovery.monica.ui.controlpanel.cropimage.model.AspectRatio\nimport cn.netdiscovery.monica.ui.controlpanel.cropimage.model.CropOutline\nimport cn.netdiscovery.monica.ui.controlpanel.cropimage.model.OutlineType\nimport cn.netdiscovery.monica.ui.controlpanel.cropimage.model.aspectRatios\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.cropimage.setting.CropDefaults\n * @author: Tony Shen\n * @date: 2024/5/26 12:20\n * @version: V1.0 <描述当前版本功能>\n */\nenum class CropType {\n    Static, Dynamic\n}\n\nobject CropDefaults {\n\n    val DefaultBackgroundColor = Color(0x99000000)\n    val DefaultOverlayColor = Color.Gray\n    val DefaultHandleColor = Color.White\n\n    /**\n     * Properties effect crop behavior that should be passed to [CropState]\n     */\n    fun properties(\n        cropType: CropType = CropType.Dynamic,\n        handleSize: Float,\n        maxZoom: Float = 10f,\n        contentScale: ContentScale = ContentScale.Fit,\n        cropOutlineProperty: CropOutlineProperty,\n        aspectRatio: AspectRatio = aspectRatios[2].aspectRatio,\n        overlayRatio: Float = .9f,\n        pannable: Boolean = true,\n        fling: Boolean = false,\n        zoomable: Boolean = true,\n        rotatable: Boolean = false,\n        fixedAspectRatio: Boolean = false,\n        requiredSize: IntSize? = null,\n        minDimension: IntSize? = null,\n    ): CropProperties {\n        return CropProperties(\n            cropType = cropType,\n            handleSize = handleSize,\n            contentScale = contentScale,\n            cropOutlineProperty = cropOutlineProperty,\n            maxZoom = maxZoom,\n            aspectRatio = aspectRatio,\n            overlayRatio = overlayRatio,\n            pannable = pannable,\n            fling = fling,\n            zoomable = zoomable,\n            rotatable = rotatable,\n            fixedAspectRatio = fixedAspectRatio,\n            requiredSize = requiredSize,\n            minDimension = minDimension,\n        )\n    }\n\n    /**\n     * Style is cosmetic changes that don't effect how [CropState] behaves because of that\n     * none of these properties are passed to [CropState]\n     */\n    fun style(\n        drawOverlay: Boolean = true,\n        drawGrid: Boolean = true,\n        strokeWidth: Dp = 1.dp,\n        overlayColor: Color = DefaultOverlayColor,\n        handleColor: Color = DefaultHandleColor,\n        backgroundColor: Color = DefaultBackgroundColor\n    ): CropStyle {\n        return CropStyle(\n            drawOverlay = drawOverlay,\n            drawGrid = drawGrid,\n            strokeWidth = strokeWidth,\n            overlayColor = overlayColor,\n            handleColor = handleColor,\n            backgroundColor = backgroundColor\n        )\n    }\n}\n\n/**\n * Data class for selecting cropper properties. Fields of this class control inner work\n * of [CropState] while some such as [cropType], [aspectRatio], [handleSize]\n * is shared between ui and state.\n */\n@Immutable\ndata class CropProperties internal constructor(\n    val cropType: CropType,\n    val handleSize: Float,\n    val contentScale: ContentScale,\n    val cropOutlineProperty: CropOutlineProperty,\n    val aspectRatio: AspectRatio,\n    val overlayRatio: Float,\n    val pannable: Boolean,\n    val fling: Boolean,\n    val rotatable: Boolean,\n    val zoomable: Boolean,\n    val maxZoom: Float,\n    val fixedAspectRatio: Boolean = false,\n    val requiredSize: IntSize? = null,\n    val minDimension: IntSize? = null,\n)\n\n/**\n * Data class for cropper styling only. None of the properties of this class is used\n * by [CropState] or [Modifier.crop]\n */\n@Immutable\ndata class CropStyle internal constructor(\n    val drawOverlay: Boolean,\n    val drawGrid: Boolean,\n    val strokeWidth: Dp,\n    val overlayColor: Color,\n    val handleColor: Color,\n    val backgroundColor: Color,\n    val cropTheme: CropTheme = CropTheme.Dark\n)\n\n/**\n * Property for passing [CropOutline] between settings UI to [ImageCropper]\n */\n@Immutable\ndata class CropOutlineProperty(\n    val outlineType: OutlineType,\n    val cropOutline: CropOutline\n)\n\n/**\n * Light, Dark or system controlled theme\n */\nenum class CropTheme{\n    Light,\n    Dark,\n    System\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/cropimage/setting/CropFrameFactory.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.cropimage.setting\n\nimport androidx.compose.runtime.mutableStateListOf\nimport androidx.compose.ui.graphics.ImageBitmap\nimport cn.netdiscovery.monica.ui.controlpanel.cropimage.utils.createPolygonShape\nimport cn.netdiscovery.monica.ui.controlpanel.cropimage.model.*\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.cropimage.setting.CropFrameFactory\n * @author: Tony Shen\n * @date: 2024/5/28 18:27\n * @version: V1.0 <描述当前版本功能>\n */\nclass CropFrameFactory(private val defaultImages: List<ImageBitmap>) {\n\n    private val cropFrames = mutableStateListOf<CropFrame>()\n\n    fun getCropFrames(): List<CropFrame> {\n        if (cropFrames.isEmpty()) {\n            val temp = mutableListOf<CropFrame>()\n            OutlineType.values().forEach {\n                temp.add(getCropFrame(it))\n            }\n            cropFrames.addAll(temp)\n        }\n        return cropFrames\n    }\n\n    fun getCropFrame(outlineType: OutlineType): CropFrame {\n        return cropFrames\n            .firstOrNull { it.outlineType == outlineType } ?: createDefaultFrame(outlineType)\n    }\n\n    private fun createDefaultFrame(outlineType: OutlineType): CropFrame {\n        return when (outlineType) {\n            OutlineType.Rect -> {\n                CropFrame(\n                    outlineType = outlineType,\n                    editable = false,\n                    cropOutlineContainer = createCropOutlineContainer(outlineType)\n                )\n            }\n\n            OutlineType.RoundedRect -> {\n                CropFrame(\n                    outlineType = outlineType,\n                    editable = true,\n                    cropOutlineContainer = createCropOutlineContainer(outlineType)\n                )\n            }\n\n            OutlineType.CutCorner -> {\n                CropFrame(\n                    outlineType = outlineType,\n                    editable = true,\n                    cropOutlineContainer = createCropOutlineContainer(outlineType)\n                )\n            }\n\n            OutlineType.Oval -> {\n                CropFrame(\n                    outlineType = outlineType,\n                    editable = true,\n                    cropOutlineContainer = createCropOutlineContainer(outlineType)\n                )\n            }\n\n            OutlineType.Polygon -> {\n                CropFrame(\n                    outlineType = outlineType,\n                    editable = true,\n                    cropOutlineContainer = createCropOutlineContainer(outlineType)\n                )\n            }\n\n            OutlineType.Parallelogram -> {\n                CropFrame(\n                    outlineType = outlineType,\n                    editable = true,\n                    cropOutlineContainer = createCropOutlineContainer(outlineType)\n                )\n            }\n\n            OutlineType.Diamond -> {\n                CropFrame(\n                    outlineType = outlineType,\n                    editable = true,\n                    cropOutlineContainer = createCropOutlineContainer(outlineType)\n                )\n            }\n\n            OutlineType.Ticket -> {\n                CropFrame(\n                    outlineType = outlineType,\n                    editable = true,\n                    cropOutlineContainer = createCropOutlineContainer(outlineType)\n                )\n            }\n\n            OutlineType.Custom -> {\n                CropFrame(\n                    outlineType = outlineType,\n                    editable = true,\n                    cropOutlineContainer = createCropOutlineContainer(outlineType)\n                )\n            }\n\n            OutlineType.ImageMask -> {\n                CropFrame(\n                    outlineType = outlineType,\n                    editable = true,\n                    cropOutlineContainer = createCropOutlineContainer(outlineType)\n                )\n            }\n        }\n    }\n\n    private fun createCropOutlineContainer(\n        outlineType: OutlineType\n    ): CropOutlineContainer<out CropOutline> {\n        return when (outlineType) {\n            OutlineType.Rect -> {\n                RectOutlineContainer(\n                    outlines = listOf(RectCropShape(id = 0, title = \"Rect\"))\n                )\n            }\n\n            OutlineType.RoundedRect -> {\n                RoundedRectOutlineContainer(\n                    outlines = listOf(RoundedCornerCropShape(id = 0, title = \"Rounded\"))\n                )\n            }\n\n            OutlineType.CutCorner -> {\n                CutCornerRectOutlineContainer(\n                    outlines = listOf(CutCornerCropShape(id = 0, title = \"CutCorner\"))\n                )\n            }\n\n            OutlineType.Oval -> {\n                OvalOutlineContainer(\n                    outlines = listOf(OvalCropShape(id = 0, title = \"Oval\"))\n                )\n            }\n\n            OutlineType.Polygon -> {\n                PolygonOutlineContainer(\n                    outlines = listOf(\n                        PolygonCropShape(\n                            id = 0,\n                            title = \"Polygon\"\n                        ),\n                        PolygonCropShape(\n                            id = 1,\n                            title = \"Triangle\",\n                            polygonProperties = PolygonProperties(sides = 3, 0f),\n                            shape = createPolygonShape(3, 30f)\n                        ),\n                        PolygonCropShape(\n                            id = 2,\n                            title = \"Pentagon\",\n                            polygonProperties = PolygonProperties(sides = 5, 0f),\n                            shape = createPolygonShape(5, 0f)\n                        ),\n                        PolygonCropShape(\n                            id = 3,\n                            title = \"Heptagon\",\n                            polygonProperties = PolygonProperties(sides = 7, 0f),\n                            shape = createPolygonShape(7, 0f)\n                        ),\n                        PolygonCropShape(\n                            id = 4,\n                            title = \"Octagon\",\n                            polygonProperties = PolygonProperties(sides = 8, 0f),\n                            shape = createPolygonShape(8, 0f)\n                        )\n                    )\n                )\n            }\n\n            OutlineType.Parallelogram -> {\n                ParallelogramOutlineContainer(\n                    outlines = listOf(ParallelogramShape(id = 0, title = \"Parallelogram\"))\n                )\n            }\n\n            OutlineType.Diamond -> {\n                DiamondOutlineContainer(\n                    outlines = listOf(DiamondShape(id = 0, title = \"Diamond\"))\n                )\n            }\n\n            OutlineType.Ticket -> {\n                TicketOutlineContainer(\n                    outlines = listOf(TicketShape(id = 0, title = \"Ticket\"))\n                )\n            }\n\n            OutlineType.Custom -> {\n                CustomOutlineContainer(\n                    outlines = listOf(\n                        CustomPathOutline(id = 0, title = \"Custom\", path = Paths.Favorite),\n                        CustomPathOutline(id = 1, title = \"Star\", path = Paths.Star)\n                    )\n                )\n            }\n\n            OutlineType.ImageMask -> {\n\n                val outlines = defaultImages.mapIndexed { index, image ->\n                    ImageMaskOutline(id = index, title = \"ImageMask\", image = image)\n\n                }\n                ImageMaskOutlineContainer(\n                    outlines = outlines\n                )\n            }\n        }\n    }\n\n    fun editCropFrame(cropFrame: CropFrame) {\n        val indexOf = cropFrames.indexOfFirst { it.outlineType == cropFrame.outlineType }\n        cropFrames[indexOf] = cropFrame\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/cropimage/setting/Paths.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.cropimage.setting\n\nimport androidx.compose.ui.graphics.Path\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.cropimage.setting.Paths\n * @author: Tony Shen\n * @date: 2024/5/28 18:29\n * @version: V1.0 <描述当前版本功能>\n */\nobject  Paths {\n    val Favorite\n        get() = Path().apply {\n            moveTo(12.0f, 21.35f)\n            relativeLineTo(-1.45f, -1.32f)\n            cubicTo(5.4f, 15.36f, 2.0f, 12.28f, 2.0f, 8.5f)\n            cubicTo(2.0f, 5.42f, 4.42f, 3.0f, 7.5f, 3.0f)\n            relativeCubicTo(1.74f, 0.0f, 3.41f, 0.81f, 4.5f, 2.09f)\n            cubicTo(13.09f, 3.81f, 14.76f, 3.0f, 16.5f, 3.0f)\n            cubicTo(19.58f, 3.0f, 22.0f, 5.42f, 22.0f, 8.5f)\n            relativeCubicTo(0.0f, 3.78f, -3.4f, 6.86f, -8.55f, 11.54f)\n            lineTo(12.0f, 21.35f)\n            close()\n        }\n\n    val Star = Path().apply {\n        moveTo(12.0f, 17.27f)\n        lineTo(18.18f, 21.0f)\n        relativeLineTo(-1.64f, -7.03f)\n        lineTo(22.0f, 9.24f)\n        relativeLineTo(-7.19f, -0.61f)\n        lineTo(12.0f, 2.0f)\n        lineTo(9.19f, 8.63f)\n        lineTo(2.0f, 9.24f)\n        relativeLineTo(5.46f, 4.73f)\n        lineTo(5.82f, 21.0f)\n        close()\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/cropimage/state/CropState.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.cropimage.state\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.unit.IntSize\nimport cn.netdiscovery.monica.ui.controlpanel.cropimage.setting.CropProperties\nimport cn.netdiscovery.monica.ui.controlpanel.cropimage.setting.CropType\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.cropimage.state.CropState\n * @author: Tony Shen\n * @date: 2024/5/26 12:04\n * @version: V1.0 <描述当前版本功能>\n */\n@Composable\nfun rememberCropState(\n    imageSize: IntSize,\n    containerSize: IntSize,\n    drawAreaSize: IntSize,\n    cropProperties: CropProperties,\n    vararg keys: Any?\n): CropState {\n\n    // Properties of crop state\n    val handleSize = cropProperties.handleSize\n    val cropType = cropProperties.cropType\n    val aspectRatio = cropProperties.aspectRatio\n    val overlayRatio = cropProperties.overlayRatio\n    val maxZoom = cropProperties.maxZoom\n    val fling = cropProperties.fling\n    val zoomable = cropProperties.zoomable\n    val pannable = cropProperties.pannable\n    val rotatable = cropProperties.rotatable\n    val fixedAspectRatio = cropProperties.fixedAspectRatio\n    val minDimension = cropProperties.minDimension\n\n    return remember(*keys) {\n        when (cropType) {\n            CropType.Static -> {\n                StaticCropState(\n                    imageSize = imageSize,\n                    containerSize = containerSize,\n                    drawAreaSize = drawAreaSize,\n                    aspectRatio = aspectRatio,\n                    overlayRatio = overlayRatio,\n                    maxZoom = maxZoom,\n                    fling = fling,\n                    zoomable = zoomable,\n                    pannable = pannable,\n                    rotatable = rotatable,\n                    limitPan = false\n                )\n            }\n            else -> {\n\n                DynamicCropState(\n                    imageSize = imageSize,\n                    containerSize = containerSize,\n                    drawAreaSize = drawAreaSize,\n                    aspectRatio = aspectRatio,\n                    overlayRatio = overlayRatio,\n                    maxZoom = maxZoom,\n                    handleSize = handleSize,\n                    fling = fling,\n                    zoomable = zoomable,\n                    pannable = pannable,\n                    rotatable = rotatable,\n                    limitPan = true,\n                    fixedAspectRatio = fixedAspectRatio,\n                    minDimension = minDimension,\n                )\n            }\n        }\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/cropimage/state/CropStateImpl.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.cropimage.state\n\nimport androidx.compose.animation.core.Animatable\nimport androidx.compose.animation.core.AnimationSpec\nimport androidx.compose.animation.core.VectorConverter\nimport androidx.compose.animation.core.tween\nimport androidx.compose.runtime.Immutable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.geometry.Rect\nimport androidx.compose.ui.geometry.Size\nimport androidx.compose.ui.input.pointer.PointerInputChange\nimport androidx.compose.ui.unit.IntSize\nimport cn.netdiscovery.monica.ui.controlpanel.cropimage.TouchRegion\nimport cn.netdiscovery.monica.ui.controlpanel.cropimage.model.AspectRatio\nimport cn.netdiscovery.monica.ui.controlpanel.cropimage.setting.CropProperties\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.cropimage.state.CropStateImpl\n * @author: Tony Shen\n * @date: 2024/5/26 12:05\n * @version: V1.0 <描述当前版本功能>\n */\n@Immutable\ndata class CropData(\n    val zoom: Float = 1f,\n    val pan: Offset = Offset.Zero,\n    val rotation: Float = 0f,\n    val overlayRect: Rect,\n    val cropRect: Rect\n)\n\nval CropState.cropData: CropData\n    get() = CropData(\n        zoom = animatableZoom.targetValue,\n        pan = Offset(animatablePanX.targetValue, animatablePanY.targetValue),\n        rotation = animatableRotation.targetValue,\n        overlayRect = overlayRect,\n        cropRect = cropRect\n    )\n\nabstract class CropState internal constructor(\n    imageSize: IntSize,\n    containerSize: IntSize,\n    drawAreaSize: IntSize,\n    maxZoom: Float,\n    internal var fling: Boolean = true,\n    internal var aspectRatio: AspectRatio,\n    internal var overlayRatio: Float,\n    zoomable: Boolean = true,\n    pannable: Boolean = true,\n    rotatable: Boolean = false,\n    limitPan: Boolean = false\n) : TransformState(\n    imageSize = imageSize,\n    containerSize = containerSize,\n    drawAreaSize = drawAreaSize,\n    initialZoom = 1f,\n    initialRotation = 0f,\n    maxZoom = maxZoom,\n    zoomable = zoomable,\n    pannable = pannable,\n    rotatable = rotatable,\n    limitPan = limitPan\n) {\n\n    private val animatableRectOverlay = Animatable(\n        getOverlayFromAspectRatio(\n            containerSize.width.toFloat(),\n            containerSize.height.toFloat(),\n            drawAreaSize.width.toFloat(),\n            aspectRatio,\n            overlayRatio\n        ),\n        Rect.VectorConverter\n    )\n\n    val overlayRect: Rect\n        get() = animatableRectOverlay.value\n\n    var cropRect: Rect = Rect.Zero\n        get() = getCropRectangle(\n            imageSize.width,\n            imageSize.height,\n            drawAreaRect,\n            animatableRectOverlay.targetValue\n        )\n        private set\n\n\n    private var initialized: Boolean = false\n\n    /**\n     * Region of touch inside, corners of or outside of overlay rectangle\n     */\n    var touchRegion by mutableStateOf(TouchRegion.None)\n\n    internal suspend fun init() {\n        // When initial aspect ratio doesn't match drawable area\n        // overlay gets updated so updates draw area as well\n        animateTransformationToOverlayBounds(overlayRect, animate = true)\n        initialized = true\n    }\n\n    /**\n     * Update properties of [CropState] and animate to valid intervals if required\n     */\n    internal open suspend fun updateProperties(\n        cropProperties: CropProperties,\n        forceUpdate: Boolean = false\n    ) {\n\n        if (!initialized) return\n\n        fling = cropProperties.fling\n        pannable = cropProperties.pannable\n        zoomable = cropProperties.zoomable\n        rotatable = cropProperties.rotatable\n\n        val maxZoom = cropProperties.maxZoom\n\n        // Update overlay rectangle\n        val aspectRatio = cropProperties.aspectRatio\n\n        // Ratio of overlay to screen\n        val overlayRatio = cropProperties.overlayRatio\n\n        if (\n            this.aspectRatio.value != aspectRatio.value ||\n            maxZoom != zoomMax ||\n            this.overlayRatio != overlayRatio ||\n            forceUpdate\n        ) {\n            this.aspectRatio = aspectRatio\n            this.overlayRatio = overlayRatio\n\n            zoomMax = maxZoom\n            animatableZoom.updateBounds(zoomMin, zoomMax)\n\n            val currentZoom = if (zoom > zoomMax) zoomMax else zoom\n\n            // Set new zoom\n            snapZoomTo(currentZoom)\n\n            // Calculate new region of image is drawn. It can be drawn left of 0 and right\n            // of container width depending on transformation\n            drawAreaRect = updateImageDrawRectFromTransformation()\n\n            // Update overlay rectangle based on current draw area and new aspect ratio\n            animateOverlayRectTo(\n                getOverlayFromAspectRatio(\n                    containerSize.width.toFloat(),\n                    containerSize.height.toFloat(),\n                    drawAreaSize.width.toFloat(),\n                    aspectRatio,\n                    overlayRatio\n                )\n            )\n        }\n\n        // Animate zoom, pan, rotation to move draw area to cover overlay rect\n        // inside draw area rect\n        animateTransformationToOverlayBounds(overlayRect, animate = true)\n    }\n\n    /**\n     * Animate overlay rectangle to target value\n     */\n    internal suspend fun animateOverlayRectTo(\n        rect: Rect,\n        animationSpec: AnimationSpec<Rect> = tween(400)\n    ) {\n        animatableRectOverlay.animateTo(\n            targetValue = rect,\n            animationSpec = animationSpec\n        )\n    }\n\n    /**\n     * Snap overlay rectangle to target value\n     */\n    internal suspend fun snapOverlayRectTo(rect: Rect) {\n        animatableRectOverlay.snapTo(rect)\n    }\n\n    /*\n        Touch gestures\n     */\n    internal abstract suspend fun onDown(change: PointerInputChange)\n\n    internal abstract suspend fun onMove(changes: List<PointerInputChange>)\n\n    internal abstract suspend fun onUp(change: PointerInputChange)\n\n    /*\n        Transform gestures\n     */\n    internal abstract suspend fun onGesture(\n        centroid: Offset,\n        panChange: Offset,\n        zoomChange: Float,\n        rotationChange: Float,\n        mainPointer: PointerInputChange,\n        changes: List<PointerInputChange>\n    )\n\n    internal abstract suspend fun onGestureStart()\n\n    internal abstract suspend fun onGestureEnd(onBoundsCalculated: () -> Unit)\n\n    // Double Tap\n    internal abstract suspend fun onDoubleTap(\n        offset: Offset,\n        zoom: Float = 1f,\n        onAnimationEnd: () -> Unit\n    )\n\n    /**\n     * Check if area that image is drawn covers [overlayRect]\n     */\n    internal fun isOverlayInImageDrawBounds(): Boolean {\n        return drawAreaRect.left <= overlayRect.left &&\n                drawAreaRect.top <= overlayRect.top &&\n                drawAreaRect.right >= overlayRect.right &&\n                drawAreaRect.bottom >= overlayRect.bottom\n    }\n\n    /**\n     * Check if [rect] is inside container bounds\n     */\n    internal fun isRectInContainerBounds(rect: Rect): Boolean {\n        return rect.left >= 0 &&\n                rect.right <= containerSize.width &&\n                rect.top >= 0 &&\n                rect.bottom <= containerSize.height\n    }\n\n    /**\n     * Update rectangle for area that image is drawn. This rect changes when zoom and\n     * pan changes and position of image changes on screen as result of transformation.\n     *\n     * This function is called\n     *\n     * * when [onGesture] is called to update rect when zoom or pan changes\n     *  and if [fling] is true just after **fling** gesture starts with target\n     *  value in  [StaticCropState].\n     *\n     *  * when [updateProperties] is called in [CropState]\n     *\n     *  * when [onUp] is called in [DynamicCropState] to match [overlayRect] that could be\n     *  changed and animated if it's out of [containerSize] bounds or its grow\n     *  bigger than previous size\n     */\n    internal fun updateImageDrawRectFromTransformation(): Rect {\n        val containerWidth = containerSize.width\n        val containerHeight = containerSize.height\n\n        val originalDrawWidth = drawAreaSize.width\n        val originalDrawHeight = drawAreaSize.height\n\n        val panX = animatablePanX.targetValue\n        val panY = animatablePanY.targetValue\n\n        val left = (containerWidth - originalDrawWidth) / 2\n        val top = (containerHeight - originalDrawHeight) / 2\n\n        val zoom = animatableZoom.targetValue\n\n        val newWidth = originalDrawWidth * zoom\n        val newHeight = originalDrawHeight * zoom\n\n        return Rect(\n            offset = Offset(\n                left - (newWidth - originalDrawWidth) / 2 + panX,\n                top - (newHeight - originalDrawHeight) / 2 + panY,\n            ),\n            size = Size(newWidth, newHeight)\n        )\n    }\n\n    // TODO Add resetting back to bounds for rotated state as well\n    /**\n     * Resets to bounds with animation and resets tracking for fling animation.\n     * Changes pan, zoom and rotation to valid bounds based on [drawAreaRect] and [overlayRect]\n     */\n    internal suspend fun animateTransformationToOverlayBounds(\n        overlayRect: Rect,\n        animate: Boolean,\n        animationSpec: AnimationSpec<Float> = tween(400)\n    ) {\n\n        val zoom = zoom.coerceAtLeast(1f)\n\n        // Calculate new pan based on overlay\n        val newDrawAreaRect = calculateValidImageDrawRect(overlayRect, drawAreaRect)\n\n        val newZoom =\n            calculateNewZoom(oldRect = drawAreaRect, newRect = newDrawAreaRect, zoom = zoom)\n\n        val leftChange = newDrawAreaRect.left - drawAreaRect.left\n        val topChange = newDrawAreaRect.top - drawAreaRect.top\n\n        val widthChange = newDrawAreaRect.width - drawAreaRect.width\n        val heightChange = newDrawAreaRect.height - drawAreaRect.height\n\n        val panXChange = leftChange + widthChange / 2\n        val panYChange = topChange + heightChange / 2\n\n        val newPanX = pan.x + panXChange\n        val newPanY = pan.y + panYChange\n\n        if (animate) {\n            resetWithAnimation(\n                pan = Offset(newPanX, newPanY),\n                zoom = newZoom,\n                animationSpec = animationSpec\n            )\n        } else {\n            snapPanXto(newPanX)\n            snapPanYto(newPanY)\n            snapZoomTo(newZoom)\n        }\n\n        resetTracking()\n\n        drawAreaRect = updateImageDrawRectFromTransformation()\n    }\n\n    /**\n     * If new overlay is bigger, when crop type is dynamic, we need to increase zoom at least\n     * size of bigger dimension for image draw area([drawAreaRect]) to cover overlay([overlayRect])\n     */\n    private fun calculateNewZoom(oldRect: Rect, newRect: Rect, zoom: Float): Float {\n\n        if (oldRect.size == Size.Zero || newRect.size == Size.Zero) return zoom\n\n        val widthChange = (newRect.width / oldRect.width)\n            .coerceAtLeast(1f)\n        val heightChange = (newRect.height / oldRect.height)\n            .coerceAtLeast(1f)\n\n        return widthChange.coerceAtLeast(heightChange) * zoom\n    }\n\n    /**\n     * Calculate valid position for image draw rectangle when pointer is up. Overlay rectangle\n     * should fit inside draw image rectangle to have valid bounds when calculation is completed.\n     *\n     * @param rectOverlay rectangle of overlay that is used for cropping\n     * @param rectDrawArea rectangle of image that is being drawn\n     */\n    private fun calculateValidImageDrawRect(rectOverlay: Rect, rectDrawArea: Rect): Rect {\n\n        var width = rectDrawArea.width\n        var height = rectDrawArea.height\n\n        if (width < rectOverlay.width) {\n            width = rectOverlay.width\n        }\n\n        if (height < rectOverlay.height) {\n            height = rectOverlay.height\n        }\n\n        var rectImageArea = Rect(offset = rectDrawArea.topLeft, size = Size(width, height))\n\n        if (rectImageArea.left > rectOverlay.left) {\n            rectImageArea = rectImageArea.translate(rectOverlay.left - rectImageArea.left, 0f)\n        }\n\n        if (rectImageArea.right < rectOverlay.right) {\n            rectImageArea = rectImageArea.translate(rectOverlay.right - rectImageArea.right, 0f)\n        }\n\n        if (rectImageArea.top > rectOverlay.top) {\n            rectImageArea = rectImageArea.translate(0f, rectOverlay.top - rectImageArea.top)\n        }\n\n        if (rectImageArea.bottom < rectOverlay.bottom) {\n            rectImageArea = rectImageArea.translate(0f, rectOverlay.bottom - rectImageArea.bottom)\n        }\n\n        return rectImageArea\n    }\n\n    /**\n     * Create [Rect] to draw overlay based on selected aspect ratio\n     */\n    internal fun getOverlayFromAspectRatio(\n        containerWidth: Float,\n        containerHeight: Float,\n        drawAreaWidth: Float,\n        aspectRatio: AspectRatio,\n        coefficient: Float\n    ): Rect {\n\n        if (aspectRatio == AspectRatio.Original) {\n            val imageAspectRatio = imageSize.width.toFloat() / imageSize.height.toFloat()\n\n            // Maximum width and height overlay rectangle can be measured with\n            val overlayWidthMax = drawAreaWidth.coerceAtMost(containerWidth * coefficient)\n            val overlayHeightMax =\n                (overlayWidthMax / imageAspectRatio).coerceAtMost(containerHeight * coefficient)\n\n            val offsetX = (containerWidth - overlayWidthMax) / 2f\n            val offsetY = (containerHeight - overlayHeightMax) / 2f\n\n            return Rect(\n                offset = Offset(offsetX, offsetY),\n                size = Size(overlayWidthMax, overlayHeightMax)\n            )\n        }\n\n        val overlayWidthMax = containerWidth * coefficient\n        val overlayHeightMax = containerHeight * coefficient\n\n        val aspectRatioValue = aspectRatio.value\n\n        var width = overlayWidthMax\n        var height = overlayWidthMax / aspectRatioValue\n\n        if (height > overlayHeightMax) {\n            height = overlayHeightMax\n            width = height * aspectRatioValue\n        }\n\n        val offsetX = (containerWidth - width) / 2f\n        val offsetY = (containerHeight - height) / 2f\n\n        return Rect(offset = Offset(offsetX, offsetY), size = Size(width, height))\n    }\n\n    /**\n     * Get crop rectangle\n     */\n    private fun getCropRectangle(\n        bitmapWidth: Int,\n        bitmapHeight: Int,\n        drawAreaRect: Rect,\n        overlayRect: Rect\n    ): Rect {\n\n        if (drawAreaRect == Rect.Zero || overlayRect == Rect.Zero) return Rect(\n            offset = Offset.Zero,\n            Size(bitmapWidth.toFloat(), bitmapHeight.toFloat())\n        )\n\n        // Calculate latest image draw area based on overlay position\n        // This is valid rectangle that contains crop area inside overlay\n        val newRect = calculateValidImageDrawRect(overlayRect, drawAreaRect)\n\n        val overlayWidth = overlayRect.width\n        val overlayHeight = overlayRect.height\n\n        val drawAreaWidth = newRect.width\n        val drawAreaHeight = newRect.height\n\n        val widthRatio = overlayWidth / drawAreaWidth\n        val heightRatio = overlayHeight / drawAreaHeight\n\n        val diffLeft = overlayRect.left - newRect.left\n        val diffTop = overlayRect.top - newRect.top\n\n        val croppedBitmapLeft = (diffLeft * (bitmapWidth / drawAreaWidth))\n        val croppedBitmapTop = (diffTop * (bitmapHeight / drawAreaHeight))\n\n        val croppedBitmapWidth = bitmapWidth * widthRatio\n        val croppedBitmapHeight = bitmapHeight * heightRatio\n\n        return Rect(\n            offset = Offset(croppedBitmapLeft, croppedBitmapTop),\n            size = Size(croppedBitmapWidth, croppedBitmapHeight)\n        )\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/cropimage/state/DynamicCropState.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.cropimage.state\n\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.geometry.Rect\nimport androidx.compose.ui.geometry.Size\nimport androidx.compose.ui.input.pointer.PointerInputChange\nimport androidx.compose.ui.input.pointer.positionChangeIgnoreConsumed\nimport androidx.compose.ui.unit.IntSize\nimport cn.netdiscovery.monica.ui.controlpanel.cropimage.TouchRegion\nimport cn.netdiscovery.monica.ui.controlpanel.cropimage.model.AspectRatio\nimport cn.netdiscovery.monica.ui.controlpanel.cropimage.setting.CropProperties\nimport kotlinx.coroutines.coroutineScope\nimport kotlin.math.roundToInt\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.cropimage.state.DynamicCropState\n * @author: Tony Shen\n * @date: 2024/5/26 15:26\n * @version: V1.0 <描述当前版本功能>\n */\nclass DynamicCropState internal constructor(\n    private var handleSize: Float,\n    imageSize: IntSize,\n    containerSize: IntSize,\n    drawAreaSize: IntSize,\n    aspectRatio: AspectRatio,\n    overlayRatio: Float,\n    maxZoom: Float,\n    fling: Boolean,\n    zoomable: Boolean,\n    pannable: Boolean,\n    rotatable: Boolean,\n    limitPan: Boolean,\n    private val fixedAspectRatio: Boolean,\n    private val minDimension: IntSize?\n) : CropState(\n    imageSize = imageSize,\n    containerSize = containerSize,\n    drawAreaSize = drawAreaSize,\n    aspectRatio = aspectRatio,\n    overlayRatio = overlayRatio,\n    maxZoom = maxZoom,\n    fling = fling,\n    zoomable = zoomable,\n    pannable = pannable,\n    rotatable = rotatable,\n    limitPan = limitPan\n) {\n\n    /**\n     * Rectangle that covers bounds of Composable. This is a rectangle uses [containerSize] as\n     * size and [Offset.Zero] as top left corner\n     */\n    private val rectBounds = Rect(\n        offset = Offset.Zero,\n        size = Size(containerSize.width.toFloat(), containerSize.height.toFloat())\n    )\n\n    // This rectangle is needed to set bounds set at first touch position while\n    // moving to constraint current bounds to temp one from first down\n    // When pointer is up\n    private var rectTemp = Rect.Zero\n\n    // Touch position for edge of the rectangle, used for not jumping to edge of rectangle\n    // when user moves a handle. We set positionActual as position of selected handle\n    // and using this distance as offset to not have a jump from touch position\n    private var distanceToEdgeFromTouch = Offset.Zero\n\n    private var doubleTapped = false\n\n    // Check if transform gesture has been invoked\n    // inside overlay but with multiple pointers to zoom\n    private var gestureInvoked = false\n\n    override suspend fun updateProperties(cropProperties: CropProperties, forceUpdate: Boolean) {\n        handleSize = cropProperties.handleSize\n        super.updateProperties(cropProperties, forceUpdate)\n    }\n\n    override suspend fun onDown(change: PointerInputChange) {\n\n        rectTemp = overlayRect.copy()\n\n        val position = change.position\n        val touchPositionScreenX = position.x\n        val touchPositionScreenY = position.y\n\n        val touchPositionOnScreen = Offset(touchPositionScreenX, touchPositionScreenY)\n\n        // Get whether user touched outside, handles of rectangle or inner region or overlay\n        // rectangle. Depending on where is touched we can move or scale overlay\n        touchRegion = getTouchRegion(\n            position = touchPositionOnScreen,\n            rect = overlayRect,\n            threshold = handleSize\n        )\n\n        // This is the difference between touch position and edge\n        // This is required for not moving edge of draw rect to touch position on move\n        distanceToEdgeFromTouch =\n            getDistanceToEdgeFromTouch(touchRegion, rectTemp, touchPositionOnScreen)\n    }\n\n    override suspend fun onMove(changes: List<PointerInputChange>) {\n\n        if (changes.isEmpty()) {\n            touchRegion = TouchRegion.None\n            return\n        }\n\n        gestureInvoked = changes.size > 1 && (touchRegion == TouchRegion.Inside)\n\n        // If overlay is touched and pointer size is one update\n        // or pointer size is bigger than one but touched any handles update\n        if (touchRegion != TouchRegion.None && changes.size == 1 && !gestureInvoked) {\n\n            val change = changes.first()\n\n            // Default min dimension is handle size * 2\n            val doubleHandleSize = handleSize * 2\n            val defaultMinDimension =\n                IntSize(doubleHandleSize.roundToInt(), doubleHandleSize.roundToInt())\n\n            // update overlay rectangle based on where its touched and touch position to corners\n            // This function moves and/or scales overlay rectangle\n            val newRect = updateOverlayRect(\n                distanceToEdgeFromTouch = distanceToEdgeFromTouch,\n                touchRegion = touchRegion,\n                minDimension = minDimension ?: defaultMinDimension,\n                rectTemp = rectTemp,\n                overlayRect = overlayRect,\n                change = change,\n                aspectRatio = getAspectRatio(),\n                fixedAspectRatio = fixedAspectRatio,\n            )\n\n            snapOverlayRectTo(newRect)\n        }\n    }\n\n    private fun getAspectRatio(): Float {\n        return if (aspectRatio == AspectRatio.Original) {\n            imageSize.width / imageSize.height.toFloat()\n        } else {\n            aspectRatio.value\n        }\n    }\n\n    override suspend fun onUp(change: PointerInputChange) = coroutineScope {\n        if (touchRegion != TouchRegion.None) {\n\n            val isInContainerBounds = isRectInContainerBounds(overlayRect)\n            if (!isInContainerBounds) {\n\n                // Calculate new overlay since it's out of Container bounds\n                rectTemp = calculateOverlayRectInBounds(rectBounds, overlayRect)\n\n                // Animate overlay to new bounds inside container\n                animateOverlayRectTo(rectTemp)\n            }\n\n            // Update and animate pan, zoom and image draw area after overlay position is updated\n            animateTransformationToOverlayBounds(overlayRect, true)\n\n            // Update image draw area after animating pan, zoom or rotation is completed\n            drawAreaRect = updateImageDrawRectFromTransformation()\n\n            touchRegion = TouchRegion.None\n        }\n\n        gestureInvoked = false\n    }\n\n    override suspend fun onGesture(\n        centroid: Offset,\n        panChange: Offset,\n        zoomChange: Float,\n        rotationChange: Float,\n        mainPointer: PointerInputChange,\n        changes: List<PointerInputChange>\n    ) {\n\n        if (touchRegion == TouchRegion.None || gestureInvoked) {\n            doubleTapped = false\n\n            val newPan = if (gestureInvoked) Offset.Zero else panChange\n\n            updateTransformState(\n                centroid = centroid,\n                zoomChange = zoomChange,\n                panChange = newPan,\n                rotationChange = rotationChange\n            )\n\n            // Update image draw rectangle based on pan, zoom or rotation change\n            drawAreaRect = updateImageDrawRectFromTransformation()\n\n            // Fling Gesture\n            if (pannable && fling) {\n                if (changes.size == 1) {\n                    addPosition(mainPointer.uptimeMillis, mainPointer.position)\n                }\n            }\n        }\n    }\n\n    override suspend fun onGestureStart() = Unit\n\n    override suspend fun onGestureEnd(onBoundsCalculated: () -> Unit) {\n\n        if (touchRegion == TouchRegion.None || gestureInvoked) {\n\n            // Gesture end might be called after second tap and we don't want to fling\n            // or animate back to valid bounds when doubled tapped\n            if (!doubleTapped) {\n\n                if (pannable && fling && !gestureInvoked && zoom > 1) {\n                    fling {\n                        // We get target value on start instead of updating bounds after\n                        // gesture has finished\n                        drawAreaRect = updateImageDrawRectFromTransformation()\n                        onBoundsCalculated()\n                    }\n                } else {\n                    onBoundsCalculated()\n                }\n\n                animateTransformationToOverlayBounds(overlayRect, animate = true)\n            }\n        }\n    }\n\n    override suspend fun onDoubleTap(\n        offset: Offset,\n        zoom: Float,\n        onAnimationEnd: () -> Unit\n    ) {\n        doubleTapped = true\n\n        if (fling) {\n            resetTracking()\n        }\n        resetWithAnimation(pan = pan, zoom = zoom, rotation = rotation)\n\n        // We get target value on start instead of updating bounds after\n        // gesture has finished\n        drawAreaRect = updateImageDrawRectFromTransformation()\n\n\n        if (!isOverlayInImageDrawBounds()) {\n            // Moves rectangle to bounds inside drawArea Rect while keeping aspect ratio\n            // of current overlay rect\n            animateOverlayRectTo(\n                getOverlayFromAspectRatio(\n                    containerSize.width.toFloat(),\n                    containerSize.height.toFloat(),\n                    drawAreaSize.width.toFloat(),\n                    aspectRatio,\n                    overlayRatio\n                )\n            )\n\n            animateTransformationToOverlayBounds(overlayRect, false)\n        }\n        onAnimationEnd()\n    }\n\n\n    // TODO Change pan when zoom is bigger than 1f and touchRegion is inside overlay rect\n//    private suspend fun moveOverlayToBounds(change: PointerInputChange, newRect: Rect) {\n//        val bounds = drawAreaRect\n//\n//        val positionChange = change.positionChangeIgnoreConsumed()\n//\n//        // When zoom is bigger than 100% and dynamic overlay is not at any edge of\n//        // image we can pan in the same direction motion goes towards when touch region\n//        // of rectangle is not one of the handles but region inside\n//        val isPanRequired = touchRegion == TouchRegion.Inside && zoom > 1f\n//\n//        // Overlay moving right\n//        if (isPanRequired && newRect.right < bounds.right) {\n//            println(\"Moving right newRect $newRect, bounds: $bounds\")\n//            snapOverlayRectTo(newRect.translate(-positionChange.x, 0f))\n//            snapPanXto(pan.x - positionChange.x * zoom)\n//            // Overlay moving left\n//        } else if (isPanRequired && pan.x < bounds.left && newRect.left <= 0f) {\n////            snapOverlayRectTo(newRect.translate(-positionChange.x, 0f))\n////            snapPanXto(pan.x - positionChange.x * zoom)\n//        } else if (isPanRequired && pan.y < bounds.top && newRect.top <= 0f) {\n//            // Overlay moving top\n////            snapOverlayRectTo(newRect.translate(0f, -positionChange.y))\n////            snapPanYto(pan.y - positionChange.y * zoom)\n//        } else if (isPanRequired && -pan.y < bounds.bottom && newRect.bottom >= containerSize.height) {\n//            // Overlay moving bottom\n////            snapOverlayRectTo(newRect.translate(0f, -positionChange.y))\n////            snapPanYto(pan.y - positionChange.y * zoom)\n//        } else {\n//            snapOverlayRectTo(newRect)\n//        }\n////        if (touchRegion != TouchRegion.None) {\n////            change.consume()\n////        }\n//    }\n\n    /**\n     * When pointer is up calculate valid position and size overlay can be updated to inside\n     * a virtual rect between `topLeft = (0,0)` to `bottomRight=(containerWidth, containerHeight)`\n     *\n     * [overlayRect] might be shrunk or moved up/down/left/right to container bounds when\n     * it's out of Composable region\n     */\n    private fun calculateOverlayRectInBounds(rectBounds: Rect, rectCurrent: Rect): Rect {\n\n        var width = rectCurrent.width\n        var height = rectCurrent.height\n\n        if (width > rectBounds.width) {\n            width = rectBounds.width\n        }\n\n        if (height > rectBounds.height) {\n            height = rectBounds.height\n        }\n\n        var rect = Rect(offset = rectCurrent.topLeft, size = Size(width, height))\n\n        if (rect.left < rectBounds.left) {\n            rect = rect.translate(rectBounds.left - rect.left, 0f)\n        }\n\n        if (rect.top < rectBounds.top) {\n            rect = rect.translate(0f, rectBounds.top - rect.top)\n        }\n\n        if (rect.right > rectBounds.right) {\n            rect = rect.translate(rectBounds.right - rect.right, 0f)\n        }\n\n        if (rect.bottom > rectBounds.bottom) {\n            rect = rect.translate(0f, rectBounds.bottom - rect.bottom)\n        }\n\n        return rect\n    }\n\n    /**\n     * Update overlay rectangle based on touch gesture\n     */\n    private fun updateOverlayRect(\n        distanceToEdgeFromTouch: Offset,\n        touchRegion: TouchRegion,\n        minDimension: IntSize,\n        rectTemp: Rect,\n        overlayRect: Rect,\n        change: PointerInputChange,\n        aspectRatio: Float,\n        fixedAspectRatio: Boolean,\n    ): Rect {\n\n        val position = change.position\n        // Get screen coordinates from touch position inside composable\n        // and add how far it's from corner to not jump edge to user's touch position\n        val screenPositionX = position.x + distanceToEdgeFromTouch.x\n        val screenPositionY = position.y + distanceToEdgeFromTouch.y\n\n        return when (touchRegion) {\n\n            // Corners\n            TouchRegion.TopLeft -> {\n\n                // Set position of top left while moving with top left handle and\n                // limit position to not intersect other handles\n                val left = screenPositionX.coerceAtMost(rectTemp.right - minDimension.width)\n                val top = if (fixedAspectRatio) {\n                    // If aspect ratio is fixed we need to calculate top position based on\n                    // left position and aspect ratio\n                    val width = rectTemp.right - left\n                    val height = width / aspectRatio\n                    rectTemp.bottom - height\n                } else {\n                    screenPositionY.coerceAtMost(rectTemp.bottom - minDimension.height)\n                }\n                Rect(\n                    left = left,\n                    top = top,\n                    right = rectTemp.right,\n                    bottom = rectTemp.bottom\n                )\n            }\n\n            TouchRegion.BottomLeft -> {\n\n                // Set position of top left while moving with bottom left handle and\n                // limit position to not intersect other handles\n                val left = screenPositionX.coerceAtMost(rectTemp.right - minDimension.width)\n                val bottom = if (fixedAspectRatio) {\n                    // If aspect ratio is fixed we need to calculate bottom position based on\n                    // left position and aspect ratio\n                    val width = rectTemp.right - left\n                    val height = width / aspectRatio\n                    rectTemp.top + height\n                } else {\n                    screenPositionY.coerceAtLeast(rectTemp.top + minDimension.height)\n                }\n                Rect(\n                    left = left,\n                    top = rectTemp.top,\n                    right = rectTemp.right,\n                    bottom = bottom,\n                )\n            }\n\n            TouchRegion.TopRight -> {\n\n                // Set position of top left while moving with top right handle and\n                // limit position to not intersect other handles\n                val right = screenPositionX.coerceAtLeast(rectTemp.left + minDimension.width)\n                val top = if (fixedAspectRatio) {\n                    // If aspect ratio is fixed we need to calculate top position based on\n                    // right position and aspect ratio\n                    val width = right - rectTemp.left\n                    val height = width / aspectRatio\n                    rectTemp.bottom - height\n                } else {\n                    screenPositionY.coerceAtMost(rectTemp.bottom - minDimension.height)\n                }\n\n                Rect(\n                    left = rectTemp.left,\n                    top = top,\n                    right = right,\n                    bottom = rectTemp.bottom,\n                )\n\n            }\n\n            TouchRegion.BottomRight -> {\n\n                // Set position of top left while moving with bottom right handle and\n                // limit position to not intersect other handles\n                val right = screenPositionX.coerceAtLeast(rectTemp.left + minDimension.width)\n                val bottom = if (fixedAspectRatio) {\n                    // If aspect ratio is fixed we need to calculate bottom position based on\n                    // right position and aspect ratio\n                    val width = right - rectTemp.left\n                    val height = width / aspectRatio\n                    rectTemp.top + height\n                } else {\n                    screenPositionY.coerceAtLeast(rectTemp.top + minDimension.height)\n                }\n\n                Rect(\n                    left = rectTemp.left,\n                    top = rectTemp.top,\n                    right = right,\n                    bottom = bottom\n                )\n            }\n\n            TouchRegion.Inside -> {\n                val drag = change.positionChangeIgnoreConsumed()\n                val scaledDragX = drag.x\n                val scaledDragY = drag.y\n                overlayRect.translate(scaledDragX, scaledDragY)\n            }\n\n            else -> overlayRect\n        }\n    }\n\n    /**\n     * get [TouchRegion] based on touch position on screen relative to [overlayRect].\n     */\n    private fun getTouchRegion(\n        position: Offset,\n        rect: Rect,\n        threshold: Float\n    ): TouchRegion {\n\n        val closedTouchRange = -threshold / 2..threshold\n\n        return when {\n            position.x - rect.left in closedTouchRange &&\n                    position.y - rect.top in closedTouchRange -> TouchRegion.TopLeft\n\n            rect.right - position.x in closedTouchRange &&\n                    position.y - rect.top in closedTouchRange -> TouchRegion.TopRight\n\n            rect.right - position.x in closedTouchRange &&\n                    rect.bottom - position.y in closedTouchRange -> TouchRegion.BottomRight\n\n            position.x - rect.left in closedTouchRange &&\n                    rect.bottom - position.y in closedTouchRange -> TouchRegion.BottomLeft\n\n\n            rect.contains(offset = position) -> TouchRegion.Inside\n            else -> TouchRegion.None\n        }\n    }\n\n    /**\n     * Returns how far user touched to corner or center of sides of the screen. [TouchRegion]\n     * where user exactly has touched is already passed to this function. For instance user\n     * touched top left then this function returns distance to top left from user's position so\n     * we can add an offset to not jump edge to position user touched.\n     */\n    private fun getDistanceToEdgeFromTouch(\n        touchRegion: TouchRegion,\n        rect: Rect,\n        touchPosition: Offset\n    ) = when (touchRegion) {\n        TouchRegion.TopLeft -> {\n            rect.topLeft - touchPosition\n        }\n        TouchRegion.TopRight -> {\n            rect.topRight - touchPosition\n        }\n        TouchRegion.BottomLeft -> {\n            rect.bottomLeft - touchPosition\n        }\n        TouchRegion.BottomRight -> {\n            rect.bottomRight - touchPosition\n        }\n        else -> {\n            Offset.Zero\n        }\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/cropimage/state/StaticCropState.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.cropimage.state\n\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.input.pointer.PointerInputChange\nimport androidx.compose.ui.unit.IntSize\nimport cn.netdiscovery.monica.ui.controlpanel.cropimage.model.AspectRatio\nimport kotlinx.coroutines.coroutineScope\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.cropimage.state.StaticCropState\n * @author: Tony Shen\n * @date: 2024/5/26 18:56\n * @version: V1.0 <描述当前版本功能>\n */\nclass StaticCropState internal constructor(\n    imageSize: IntSize,\n    containerSize: IntSize,\n    drawAreaSize: IntSize,\n    aspectRatio: AspectRatio,\n    overlayRatio: Float,\n    maxZoom: Float = 5f,\n    fling: Boolean = false,\n    zoomable: Boolean = true,\n    pannable: Boolean = true,\n    rotatable: Boolean = false,\n    limitPan: Boolean = false\n) : CropState(\n    imageSize = imageSize,\n    containerSize = containerSize,\n    drawAreaSize = drawAreaSize,\n    aspectRatio = aspectRatio,\n    overlayRatio = overlayRatio,\n    maxZoom = maxZoom,\n    fling = fling,\n    zoomable = zoomable,\n    pannable = pannable,\n    rotatable = rotatable,\n    limitPan = limitPan\n) {\n\n    override suspend fun onDown(change: PointerInputChange) = Unit\n    override suspend fun onMove(changes: List<PointerInputChange>) = Unit\n    override suspend fun onUp(change: PointerInputChange) = Unit\n\n    private var doubleTapped = false\n\n    /*\n        Transform gestures\n    */\n    override suspend fun onGesture(\n        centroid: Offset,\n        panChange: Offset,\n        zoomChange: Float,\n        rotationChange: Float,\n        mainPointer: PointerInputChange,\n        changes: List<PointerInputChange>\n    ) = coroutineScope {\n        doubleTapped = false\n\n        updateTransformState(\n            centroid = centroid,\n            zoomChange = zoomChange,\n            panChange = panChange,\n            rotationChange = rotationChange\n        )\n\n        // Update image draw rectangle based on pan, zoom or rotation change\n        drawAreaRect = updateImageDrawRectFromTransformation()\n\n        // Fling Gesture\n        if (pannable && fling) {\n            if (changes.size == 1) {\n                addPosition(mainPointer.uptimeMillis, mainPointer.position)\n            }\n        }\n    }\n\n    override suspend fun onGestureStart() = coroutineScope {}\n\n    override suspend fun onGestureEnd(onBoundsCalculated: () -> Unit) {\n\n        // Gesture end might be called after second tap and we don't want to fling\n        // or animate back to valid bounds when doubled tapped\n        if (!doubleTapped) {\n\n            if (pannable && fling && zoom > 1) {\n                fling {\n                    // We get target value on start instead of updating bounds after\n                    // gesture has finished\n                    drawAreaRect = updateImageDrawRectFromTransformation()\n                    onBoundsCalculated()\n                }\n            } else {\n                onBoundsCalculated()\n            }\n\n            animateTransformationToOverlayBounds(overlayRect, animate = true)\n        }\n    }\n\n    // Double Tap\n    override suspend fun onDoubleTap(\n        offset: Offset,\n        zoom: Float,\n        onAnimationEnd: () -> Unit\n    ) {\n        doubleTapped = true\n\n        if (fling) {\n            resetTracking()\n        }\n        resetWithAnimation(pan = pan, zoom = zoom, rotation = rotation)\n        drawAreaRect = updateImageDrawRectFromTransformation()\n        animateTransformationToOverlayBounds(overlayRect, true)\n        onAnimationEnd()\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/cropimage/state/TransformState.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.cropimage.state\n\nimport androidx.compose.animation.core.Animatable\nimport androidx.compose.animation.core.AnimationSpec\nimport androidx.compose.animation.core.exponentialDecay\nimport androidx.compose.animation.core.tween\nimport androidx.compose.runtime.Stable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.geometry.Rect\nimport androidx.compose.ui.geometry.Size\nimport androidx.compose.ui.input.pointer.util.VelocityTracker\nimport androidx.compose.ui.unit.IntSize\nimport kotlinx.coroutines.coroutineScope\nimport kotlinx.coroutines.launch\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.cropimage.state.TransformState\n * @author: Tony Shen\n * @date: 2024/5/26 12:09\n * @version: V1.0 <描述当前版本功能>\n */\n@Stable\nopen class TransformState(\n    internal val imageSize: IntSize,\n    val containerSize: IntSize,\n    val drawAreaSize: IntSize,\n    initialZoom: Float = 1f,\n    initialRotation: Float = 0f,\n    minZoom: Float = 1f,\n    maxZoom: Float = 10f,\n    internal var zoomable: Boolean = true,\n    internal var pannable: Boolean = true,\n    internal var rotatable: Boolean = true,\n    internal var limitPan: Boolean = false\n) {\n\n    var drawAreaRect: Rect by mutableStateOf(\n        Rect(\n            offset = Offset(\n                x = ((containerSize.width - drawAreaSize.width) / 2).toFloat(),\n                y = ((containerSize.height - drawAreaSize.height) / 2).toFloat()\n            ),\n            size = Size(drawAreaSize.width.toFloat(), drawAreaSize.height.toFloat())\n        )\n    )\n\n    internal val zoomMin = minZoom.coerceAtLeast(1f)\n    internal var zoomMax = maxZoom.coerceAtLeast(1f)\n    private val zoomInitial = initialZoom.coerceIn(zoomMin, zoomMax)\n    private val rotationInitial = initialRotation % 360\n\n    internal val animatablePanX = Animatable(0f)\n    internal val animatablePanY = Animatable(0f)\n    internal val animatableZoom = Animatable(zoomInitial)\n    internal val animatableRotation = Animatable(rotationInitial)\n\n    private val velocityTracker = VelocityTracker()\n\n    init {\n        animatableZoom.updateBounds(zoomMin, zoomMax)\n        require(zoomMax >= zoomMin)\n    }\n\n    val pan: Offset\n        get() = Offset(animatablePanX.value, animatablePanY.value)\n\n    val zoom: Float\n        get() = animatableZoom.value\n\n    val rotation: Float\n        get() = animatableRotation.value\n\n    val isZooming: Boolean\n        get() = animatableZoom.isRunning\n\n    val isPanning: Boolean\n        get() = animatablePanX.isRunning || animatablePanY.isRunning\n\n    val isRotating: Boolean\n        get() = animatableRotation.isRunning\n\n    val isAnimationRunning: Boolean\n        get() = isZooming || isPanning || isRotating\n\n    internal open fun updateBounds(lowerBound: Offset?, upperBound: Offset?) {\n        animatablePanX.updateBounds(lowerBound?.x, upperBound?.x)\n        animatablePanY.updateBounds(lowerBound?.y, upperBound?.y)\n    }\n\n    /**\n     * Update centroid, pan, zoom and rotation of this state when transform gestures are\n     * invoked with one or multiple pointers\n     */\n    internal open suspend fun updateTransformState(\n        centroid: Offset,\n        panChange: Offset,\n        zoomChange: Float,\n        rotationChange: Float = 1f,\n    ) {\n        val newZoom = (this.zoom * zoomChange).coerceIn(zoomMin, zoomMax)\n\n        snapZoomTo(newZoom)\n        val newRotation = if (rotatable) {\n            this.rotation + rotationChange\n        } else {\n            0f\n        }\n        snapRotationTo(newRotation)\n\n        if (pannable) {\n            val newPan = this.pan + panChange.times(this.zoom)\n            snapPanXto(newPan.x)\n            snapPanYto(newPan.y)\n        }\n    }\n\n    /**\n     * Reset [pan], [zoom] and [rotation] with animation.\n     */\n    internal suspend fun resetWithAnimation(\n        pan: Offset = Offset.Zero,\n        zoom: Float = 1f,\n        rotation: Float = 0f,\n        animationSpec: AnimationSpec<Float> = tween(400)\n    ) = coroutineScope {\n        launch { animatePanXto(pan.x, animationSpec) }\n        launch { animatePanYto(pan.y, animationSpec) }\n        launch { animateZoomTo(zoom, animationSpec) }\n        launch { animateRotationTo(rotation, animationSpec) }\n    }\n\n    internal suspend fun animatePanXto(\n        panX: Float,\n        animationSpec: AnimationSpec<Float> = tween(400)\n    ) {\n        if (pannable && pan.x != panX) {\n            animatablePanX.animateTo(panX, animationSpec)\n        }\n    }\n\n    internal suspend fun animatePanYto(\n        panY: Float,\n        animationSpec: AnimationSpec<Float> = tween(400)\n    ) {\n        if (pannable && pan.y != panY) {\n            animatablePanY.animateTo(panY, animationSpec)\n        }\n    }\n\n    internal suspend fun animateZoomTo(\n        zoom: Float,\n        animationSpec: AnimationSpec<Float> = tween(400)\n    ) {\n        if (zoomable && this.zoom != zoom) {\n            val newZoom = zoom.coerceIn(zoomMin, zoomMax)\n            animatableZoom.animateTo(newZoom, animationSpec)\n        }\n    }\n\n    internal suspend fun animateRotationTo(\n        rotation: Float,\n        animationSpec: AnimationSpec<Float> = tween(400)\n    ) {\n        if (rotatable && this.rotation != rotation) {\n            animatableRotation.animateTo(rotation, animationSpec)\n        }\n    }\n\n    internal suspend fun snapPanXto(panX: Float) {\n        if (pannable) {\n            animatablePanX.snapTo(panX)\n        }\n    }\n\n    internal suspend fun snapPanYto(panY: Float) {\n        if (pannable) {\n            animatablePanY.snapTo(panY)\n        }\n    }\n\n    internal suspend fun snapZoomTo(zoom: Float) {\n        if (zoomable) {\n            animatableZoom.snapTo(zoom.coerceIn(zoomMin, zoomMax))\n        }\n    }\n\n    internal suspend fun snapRotationTo(rotation: Float) {\n        if (rotatable) {\n            animatableRotation.snapTo(rotation)\n        }\n    }\n\n    /*\n    Fling gesture\n */\n    internal fun addPosition(timeMillis: Long, position: Offset) {\n        velocityTracker.addPosition(\n            timeMillis = timeMillis,\n            position = position\n        )\n    }\n\n    /**\n     * Create a fling gesture when user removes finger from scree to have continuous movement\n     * until [velocityTracker] speed reached to lower bound\n     */\n    internal suspend fun fling(onFlingStart: () -> Unit) = coroutineScope {\n        val velocityTracker = velocityTracker.calculateVelocity()\n        val velocity = Offset(velocityTracker.x, velocityTracker.y)\n        var flingStarted = false\n\n        launch {\n            animatablePanX.animateDecay(\n                velocity.x,\n                exponentialDecay(absVelocityThreshold = 20f),\n                block = {\n                    // This callback returns target value of fling gesture initially\n                    if (!flingStarted) {\n                        onFlingStart()\n                        flingStarted = true\n                    }\n                }\n            )\n        }\n\n        launch {\n            animatablePanY.animateDecay(\n                velocity.y,\n                exponentialDecay(absVelocityThreshold = 20f),\n                block = {\n                    // This callback returns target value of fling gesture initially\n                    if (!flingStarted) {\n                        onFlingStart()\n                        flingStarted = true\n                    }\n                }\n            )\n        }\n    }\n\n    internal fun resetTracking() {\n        velocityTracker.resetTracking()\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/cropimage/utils/DrawScopeUtils.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.cropimage.utils\n\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.geometry.Rect\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.drawscope.DrawScope\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.cropimage.utils.DrawScopeUtils\n * @author: Tony Shen\n * @date: 2024/5/26 15:43\n * @version: V1.0 <描述当前版本功能>\n */\nfun DrawScope.drawGrid(rect: Rect, strokeWidth: Float, color: Color) {\n\n    val width = rect.width\n    val height = rect.height\n    val gridWidth = width / 3\n    val gridHeight = height / 3\n\n    // Horizontal lines\n    for (i in 1..2) {\n        drawLine(\n            color = color,\n            start = Offset(rect.left, rect.top + i * gridHeight),\n            end = Offset(rect.right, rect.top + i * gridHeight),\n            strokeWidth = strokeWidth\n        )\n    }\n\n    // Vertical lines\n    for (i in 1..2) {\n        drawLine(\n            color,\n            start = Offset(rect.left + i * gridWidth, rect.top),\n            end = Offset(rect.left + i * gridWidth, rect.bottom),\n            strokeWidth = strokeWidth\n        )\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/cropimage/utils/ShapeUtils.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.cropimage.utils\n\nimport androidx.compose.foundation.shape.GenericShape\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.geometry.Rect\nimport androidx.compose.ui.geometry.Size\nimport androidx.compose.ui.graphics.*\nimport androidx.compose.ui.unit.Density\nimport androidx.compose.ui.unit.LayoutDirection\nimport cn.netdiscovery.monica.ui.controlpanel.cropimage.model.AspectRatio\nimport org.jetbrains.skia.Matrix33\nimport kotlin.math.cos\nimport kotlin.math.sin\nimport kotlin.math.tan\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.cropimage.utils.ShapeUtils\n * @author: Tony Shen\n * @date: 2024/5/26 12:12\n * @version: V1.0 <描述当前版本功能>\n */\n\nfun createPolygonPath(cx: Float, cy: Float, sides: Int, radius: Float): Path {\n\n    val angle = 2.0 * Math.PI / sides\n\n    return Path().apply {\n        moveTo(\n            cx + (radius * cos(0.0)).toFloat(),\n            cy + (radius * sin(0.0)).toFloat()\n        )\n        for (i in 1 until sides) {\n            lineTo(\n                cx + (radius * cos(angle * i)).toFloat(),\n                cy + (radius * sin(angle * i)).toFloat()\n            )\n        }\n        close()\n    }\n}\n\nfun createPolygonShape(sides: Int, degrees: Float = 0f): GenericShape {\n    return GenericShape { size: Size, _: LayoutDirection ->\n\n        val radius = size.width.coerceAtMost(size.height) / 2\n        addPath(\n            createPolygonPath(\n                cx = size.width / 2,\n                cy = size.height / 2,\n                sides = sides,\n                radius = radius\n            )\n        )\n\n        val matrix = Matrix33.makeRotate(degrees, size.width / 2, size.height / 2)\n        this.asSkiaPath().transform(matrix)\n    }\n}\n\n\n/**\n * Creates a [Rect] shape with given aspect ratio.\n */\nfun createRectShape(aspectRatio: AspectRatio): GenericShape {\n    return GenericShape { size: Size, _: LayoutDirection ->\n        val value = aspectRatio.value\n\n        val width = size.width\n        val height = size.height\n        val shapeSize =\n            if (aspectRatio == AspectRatio.Original) Size(width, height)\n            else if (value > 1) Size(width = width, height = width / value)\n            else Size(width = height * value, height = height)\n\n        addRect(Rect(offset = Offset.Zero, size = shapeSize))\n    }\n}\n\nfun Path.scaleAndTranslatePath(\n    width: Float,\n    height: Float,\n) {\n    val pathSize = getBounds().size\n\n    val matrix = Matrix33.makeScale(\n        width / pathSize.width,\n        height / pathSize.height\n    )\n\n    this.asSkiaPath().transform(matrix)\n\n    val left = getBounds().left\n    val top = getBounds().top\n\n    translate(Offset(-left, -top))\n}\n\n/**\n * Build an outline from a shape using aspect ratio, shape and coefficient to scale\n *\n * @return [Triple] that contains left, top offset and [Outline]\n */\nfun buildOutline(\n    aspectRatio: AspectRatio,\n    coefficient: Float,\n    shape: Shape,\n    size: Size,\n    layoutDirection: LayoutDirection,\n    density: Density\n): Pair<Offset, Outline> {\n\n    val (shapeSize, offset) = calculateSizeAndOffsetFromAspectRatio(aspectRatio, coefficient, size)\n\n    val outline = shape.createOutline(\n        size = shapeSize,\n        layoutDirection = layoutDirection,\n        density = density\n    )\n    return Pair(offset, outline)\n}\n\n\n/**\n * Calculate new size and offset based on [size], [coefficient] and [aspectRatio]\n *\n * For 4/3f aspect ratio with 1000px width, 1000px height with coefficient 1f\n * it returns Size(1000f, 750f), Offset(0f, 125f).\n */\nfun calculateSizeAndOffsetFromAspectRatio(\n    aspectRatio: AspectRatio,\n    coefficient: Float,\n    size: Size,\n): Pair<Size, Offset> {\n    val width = size.width\n    val height = size.height\n\n    val value = aspectRatio.value\n\n    val newSize = if (aspectRatio == AspectRatio.Original) {\n        Size(width * coefficient, height * coefficient)\n    } else if (value > 1) {\n        Size(\n            width = coefficient * width,\n            height = coefficient * width / value\n        )\n    } else {\n        Size(width = coefficient * height * value, height = coefficient * height)\n    }\n\n    val left = (width - newSize.width) / 2\n    val top = (height - newSize.height) / 2\n\n    return Pair(newSize, Offset(left, top))\n}\n\nclass Parallelogram(private val angle: Float) : Shape {\n    override fun createOutline(\n        size: Size,\n        layoutDirection: LayoutDirection,\n        density: Density\n    ): Outline {\n        return Outline.Generic(\n\n            Path().apply {\n                val radian = (90 - angle) * Math.PI / 180\n                val xOnOpposite = (size.height * tan(radian)).toFloat()\n                moveTo(0f, size.height)\n                lineTo(x = xOnOpposite, y = 0f)\n                lineTo(x = size.width, y = 0f)\n                lineTo(x = size.width - xOnOpposite, y = size.height)\n                lineTo(x = xOnOpposite, y = size.height)\n            }\n        )\n    }\n}\n\nclass Diamond : Shape {\n\n    /**\n     * Creates the [Outline] for the diamond shape.\n     *\n     * @param size The [Size] of the diamond.\n     * @param layoutDirection The [LayoutDirection] of the diamond.\n     * @param density The [Density] of the diamond.\n     * @return The [Outline] representing the diamond shape.\n     */\n    override fun createOutline(\n        size: Size,\n        layoutDirection: LayoutDirection,\n        density: Density\n    ): Outline {\n        return Outline.Generic(\n\n            Path().apply {\n                val centerX = size.width / 2f\n                val diamondCurve = 60f\n                val width = size.width\n                val height = size.height\n\n                moveTo(x = 0f + diamondCurve, y = 0f)\n                lineTo(x = width - diamondCurve, y = 0f)\n                lineTo(x = width, y = diamondCurve)\n                lineTo(x = centerX, y = height)\n                lineTo(x = 0f, y = diamondCurve)\n\n                close()\n            }\n        )\n    }\n}\n\nclass Ticket : Shape {\n\n    /**\n     * Creates the [Outline] for the ticket shape with rounded corners.\n     *\n     * @param size The [Size] of the shape.\n     * @param layoutDirection The [LayoutDirection] of the shape.\n     * @param density The [Density] of the shape.\n     * @return The [Outline] representing the ticket shape with rounded corners.\n     */\n    override fun createOutline(\n        size: Size,\n        layoutDirection: LayoutDirection,\n        density: Density\n    ): Outline {\n        return Outline.Generic(\n\n            Path().apply {\n                val cornerRadius = 70f\n                // Top left arc\n                arcTo(\n                    rect = Rect(\n                        left = -cornerRadius,\n                        top = -cornerRadius,\n                        right = cornerRadius,\n                        bottom = cornerRadius\n                    ),\n                    startAngleDegrees = 90.0f,\n                    sweepAngleDegrees = -90.0f,\n                    forceMoveTo = false\n                )\n                lineTo(x = size.width - cornerRadius, y = 0f)\n                // Top right arc\n                arcTo(\n                    rect = Rect(\n                        left = size.width - cornerRadius,\n                        top = -cornerRadius,\n                        right = size.width + cornerRadius,\n                        bottom = cornerRadius\n                    ),\n                    startAngleDegrees = 180.0f,\n                    sweepAngleDegrees = -90.0f,\n                    forceMoveTo = false\n                )\n                lineTo(x = size.width, y = size.height - cornerRadius)\n                // Bottom right arc\n                arcTo(\n                    rect = Rect(\n                        left = size.width - cornerRadius,\n                        top = size.height - cornerRadius,\n                        right = size.width + cornerRadius,\n                        bottom = size.height + cornerRadius\n                    ),\n                    startAngleDegrees = 270.0f,\n                    sweepAngleDegrees = -90.0f,\n                    forceMoveTo = false\n                )\n                lineTo(x = cornerRadius, y = size.height)\n                // Bottom left arc\n                arcTo(\n                    rect = Rect(\n                        left = -cornerRadius,\n                        top = size.height - cornerRadius,\n                        right = cornerRadius,\n                        bottom = size.height + cornerRadius\n                    ),\n                    startAngleDegrees = 0.0f,\n                    sweepAngleDegrees = -90.0f,\n                    forceMoveTo = false\n                )\n                lineTo(x = 0f, y = cornerRadius)\n\n                close()\n            }\n        )\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/cropimage/utils/ZoomUtils.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.cropimage.utils\n\nimport androidx.compose.ui.graphics.GraphicsLayerScope\nimport cn.netdiscovery.monica.ui.controlpanel.cropimage.state.TransformState\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.cropimage.utils.ZoomUtils\n * @author: Tony Shen\n * @date: 2024/5/26 15:32\n * @version: V1.0 <描述当前版本功能>\n */\nenum class ZoomLevel {\n    Min, Mid, Max\n}\n\ninternal fun getNextZoomLevel(zoomLevel: ZoomLevel): ZoomLevel = when (zoomLevel) {\n    ZoomLevel.Mid -> ZoomLevel.Max\n    ZoomLevel.Max -> ZoomLevel.Min\n    else          -> ZoomLevel.Mid\n}\n\n/**\n * Update graphic layer with [transformState]\n */\ninternal fun GraphicsLayerScope.update(transformState: TransformState) {\n\n    // Set zoom\n    val zoom = transformState.zoom\n    this.scaleX = zoom\n    this.scaleY = zoom\n\n    // Set pan\n    val pan = transformState.pan\n    val translationX = pan.x\n    val translationY = pan.y\n    this.translationX = translationX\n    this.translationY = translationY\n\n    // Set rotation\n    this.rotationZ = transformState.rotation\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/doodle/DoodleView.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.doodle\n\nimport androidx.compose.foundation.Canvas\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.shadow\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.graphics.*\nimport androidx.compose.ui.graphics.drawscope.Stroke\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.unit.IntSize\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.material.MaterialTheme\nimport androidx.compose.material.Text\nimport cn.netdiscovery.monica.state.ApplicationState\nimport cn.netdiscovery.monica.ui.controlpanel.doodle.model.PathProperties\nimport cn.netdiscovery.monica.ui.widget.color.ColorSelectionDialog\nimport cn.netdiscovery.monica.ui.controlpanel.doodle.widget.PropertiesMenuDialog\nimport cn.netdiscovery.monica.ui.widget.image.gesture.MotionEvent\nimport cn.netdiscovery.monica.ui.widget.image.gesture.dragMotionEvent\nimport cn.netdiscovery.monica.ui.widget.rightSideMenuBar\nimport cn.netdiscovery.monica.ui.widget.toolTipButton\nimport cn.netdiscovery.monica.ui.widget.image.ImageSizeCalculator\nimport cn.netdiscovery.monica.i18n.getCurrentStringResource\nimport org.koin.compose.koinInject\nimport org.slf4j.Logger\nimport org.slf4j.LoggerFactory\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.showimage.DoodleView\n * @author: Tony Shen\n * @date:  2024/5/19 21:11\n * @version: V1.0 <描述当前版本功能>\n */\nprivate val logger: Logger = LoggerFactory.getLogger(object : Any() {}.javaClass.enclosingClass)\n\n@Composable\nfun drawImage(\n    state: ApplicationState\n) {\n    val viewModel: DoodleViewModel = koinInject()\n\n    val density = LocalDensity.current\n    val i18nState = getCurrentStringResource()\n\n    // 双路径系统：displayPaths用于显示，originalPaths用于保存\n    val displayPaths = remember { mutableStateListOf<Pair<Path, PathProperties>>() }\n    val originalPaths = remember { mutableStateListOf<Pair<Path, PathProperties>>() }\n    val pathsUndone = remember { mutableStateListOf<Pair<Pair<Path, PathProperties>, Pair<Path, PathProperties>>>() }\n    \n    // 分离当前绘制状态，避免与已完成路径的相互影响\n    val currentDrawingPath = remember { mutableStateOf<Pair<Path, PathProperties>?>(null) }\n    \n    // 撤销历史限制\n    val maxUndoHistory = 50\n\n    var motionEvent by remember { mutableStateOf(MotionEvent.Idle) }\n    // This is our motion event we get from touch motion\n    var currentPosition by remember { mutableStateOf(Offset.Unspecified) }\n    // This is previous motion event before next touch is saved into this current position\n    var previousPosition by remember { mutableStateOf(Offset.Unspecified) }\n    var currentDisplayPath by remember { mutableStateOf(Path()) }\n    var currentOriginalPath by remember { mutableStateOf(Path()) }\n    var currentPathProperty by remember { mutableStateOf(PathProperties()) }\n\n    var showColorDialog by remember { mutableStateOf(false) }\n    var showPropertiesDialog by remember { mutableStateOf(false) }\n\n    // 使用更直接的状态管理\n    val drawingState = remember { mutableStateOf(Triple(MotionEvent.Idle, Offset.Unspecified, Path())) }\n\n    // 安全获取图片，避免空指针异常\n    val image = state.currentImage?.toComposeImageBitmap()\n    \n    // 如果图片为空，显示提示信息\n    if (image == null) {\n        Box(\n            Modifier.fillMaxSize(),\n            contentAlignment = Alignment.Center\n        ) {\n            Text(\n                text = \"请先加载图片\",\n                color = Color.Gray\n            )\n        }\n        return\n    }\n\n    Box(\n        modifier = Modifier\n            .fillMaxSize()\n            .verticalScroll(rememberScrollState())\n            .background(\n                brush = Brush.verticalGradient(\n                    colors = listOf(\n                        MaterialTheme.colors.background,\n                        MaterialTheme.colors.surface\n                    )\n                )\n            ),\n        contentAlignment = Alignment.Center\n    ) {\n        // 使用统一的图片尺寸计算\n        val (width, height) = ImageSizeCalculator.calculateImageSize(state)\n        \n        // 获取原始图片尺寸和显示尺寸，用于保存时的坐标转换\n        val originalSize = ImageSizeCalculator.getImagePixelSize(state)\n        val displaySize = ImageSizeCalculator.getImageDisplayPixelSize(state, density.density)\n        \n        // 预计算缩放比例，避免重复计算\n        val scaleX = if (originalSize != null && displaySize != null) {\n            originalSize.first.toFloat() / displaySize.first.toFloat()\n        } else 1f\n        val scaleY = if (originalSize != null && displaySize != null) {\n            originalSize.second.toFloat() / displaySize.second.toFloat()\n        } else 1f\n\n        Column(\n            modifier = Modifier.align(Alignment.Center).width(width).height(height),\n            verticalArrangement = Arrangement.Center,\n            horizontalAlignment = Alignment.CenterHorizontally\n        ) {\n            val drawModifier = Modifier\n                .padding(8.dp)\n                .shadow(1.dp)\n                .fillMaxWidth()\n                .weight(1f)\n                .background(Color.White)\n                .dragMotionEvent(\n                    onDragStart = { pointerInputChange ->\n                        motionEvent = MotionEvent.Down\n                        currentPosition = pointerInputChange.position\n                        \n                        // 显示路径使用显示坐标（用于实时显示）\n                        currentDisplayPath.moveTo(currentPosition.x, currentPosition.y)\n                        \n                        // 原始路径使用原始坐标（用于保存）\n                        val originalPosition = Offset(currentPosition.x * scaleX, currentPosition.y * scaleY)\n                        currentOriginalPath.moveTo(originalPosition.x, originalPosition.y)\n                        \n                        // 更新分离的绘制状态\n                        currentDrawingPath.value = Pair(currentDisplayPath, currentPathProperty)\n                        \n                        previousPosition = currentPosition\n                        pointerInputChange.consume()\n                    },\n                    onDrag = { pointerInputChange ->\n                        val newPosition = pointerInputChange.position\n                        \n                        // 立即更新状态，确保实时响应\n                        motionEvent = MotionEvent.Move\n                        currentPosition = newPosition\n                        \n                        if (previousPosition != Offset.Unspecified) {\n                            // 使用quadraticBezierTo绘制平滑曲线\n                            val midX = (previousPosition.x + currentPosition.x) / 2\n                            val midY = (previousPosition.y + currentPosition.y) / 2\n                            \n                            // 显示路径使用显示坐标（用于实时显示）\n                            currentDisplayPath.quadraticBezierTo(previousPosition.x, previousPosition.y, midX, midY)\n                            \n                            // 原始路径使用原始坐标（用于保存）\n                            val originalPosition = Offset(currentPosition.x * scaleX, currentPosition.y * scaleY)\n                            val originalPreviousPosition = Offset(previousPosition.x * scaleX, previousPosition.y * scaleY)\n                            val originalMidX = (originalPreviousPosition.x + originalPosition.x) / 2\n                            val originalMidY = (originalPreviousPosition.y + originalPosition.y) / 2\n                            currentOriginalPath.quadraticBezierTo(originalPreviousPosition.x, originalPreviousPosition.y, originalMidX, originalMidY)\n\n                            previousPosition = currentPosition\n                            \n                            // 更新分离的绘制状态 - 创建新的Path对象确保实时更新\n                            val newDisplayPath = Path().apply {\n                                addPath(currentDisplayPath)\n                            }\n                            currentDrawingPath.value = Pair(newDisplayPath, currentPathProperty)\n                        }\n                        pointerInputChange.consume()\n                    },\n                    onDragEnd = { pointerInputChange ->\n                        motionEvent = MotionEvent.Up\n                        \n                        // 显示路径使用显示坐标（用于实时显示）\n                        currentDisplayPath.lineTo(currentPosition.x, currentPosition.y)\n                        \n                        // 原始路径使用原始坐标（用于保存）\n                        val originalPosition = Offset(currentPosition.x * scaleX, currentPosition.y * scaleY)\n                        currentOriginalPath.lineTo(originalPosition.x, originalPosition.y)\n\n                        // 同时保存显示路径和原始路径\n                        // 创建PathProperties的副本，避免引用共享\n                        val pathPropertyCopy = PathProperties(\n                            strokeWidth = currentPathProperty.strokeWidth,\n                            color = Color(currentPathProperty.color.red, currentPathProperty.color.green, currentPathProperty.color.blue, currentPathProperty.color.alpha),\n                            alpha = currentPathProperty.alpha,\n                            strokeCap = currentPathProperty.strokeCap,\n                            strokeJoin = currentPathProperty.strokeJoin\n                        )\n                        displayPaths.add(Pair(currentDisplayPath, pathPropertyCopy))\n                        originalPaths.add(Pair(currentOriginalPath, pathPropertyCopy))\n                        \n                        logger.info(\"路径已添加，当前路径数量: displayPaths=${displayPaths.size}, originalPaths=${originalPaths.size}\")\n                        logger.info(\"保存的路径颜色: ${pathPropertyCopy.color}\")\n                        \n                        // 清空当前绘制状态\n                        currentDrawingPath.value = null\n                        \n                        // 重置路径\n                        currentDisplayPath = Path()\n                        currentOriginalPath = Path()\n                        // 保持当前的颜色设置，不重置currentPathProperty\n\n                        // 限制撤销历史数量，防止内存溢出\n                        if (pathsUndone.size >= maxUndoHistory) {\n                            pathsUndone.removeAt(0)\n                        }\n                        // 注意：不要清空撤销历史，让用户可以撤销之前的操作\n\n                        currentPosition = Offset.Unspecified\n                        previousPosition = currentPosition\n                        motionEvent = MotionEvent.Idle\n                        pointerInputChange.consume()\n                    }\n                )\n\n            Canvas(modifier = drawModifier) {\n                this.drawImage(image = image,\n                    dstSize = IntSize(width.toPx().toInt(), height.toPx().toInt()))\n\n                // 绘制已完成的路径（使用显示路径）\n                // 使用key来确保路径变化时能正确重绘\n                displayPaths.forEachIndexed { index, pathPair ->\n                    val path = pathPair.first\n                    val property = pathPair.second\n\n                    drawPath(\n                        color = property.color,\n                        path = path,\n                        style = Stroke(\n                            width = property.strokeWidth,\n                            cap = property.strokeCap,\n                            join = property.strokeJoin\n                        )\n                    )\n                }\n\n                // 绘制当前正在绘制的路径（使用分离的状态）\n                currentDrawingPath.value?.let { (currentPath, currentProps) ->\n                    drawPath(\n                        color = currentProps.color,\n                        path = currentPath,\n                        style = Stroke(\n                            width = currentProps.strokeWidth,\n                            cap = currentProps.strokeCap,\n                            join = currentProps.strokeJoin\n                        )\n                    )\n                }\n            }\n        }\n\n        rightSideMenuBar(modifier = Modifier.align(Alignment.CenterEnd)) {\n            // 选择颜色\n            toolTipButton(text = i18nState.get(\"select_color\"),\n                painter = painterResource(\"images/doodle/color.png\"),\n                onClick = {\n                    showColorDialog = true\n                })\n\n            // 属性更改\n            toolTipButton(text = i18nState.get(\"change_properties\"),\n                painter = painterResource(\"images/doodle/brush.png\"),\n                onClick = {\n                    showPropertiesDialog = true\n                })\n\n            // 上一步\n            toolTipButton(text = i18nState.get(\"previous_step\"),\n                painter = painterResource(\"images/doodle/previous_step.png\"),\n                onClick = {\n                    logger.info(\"撤销前状态: displayPaths=${displayPaths.size}, originalPaths=${originalPaths.size}\")\n                    if (displayPaths.isNotEmpty() && originalPaths.isNotEmpty()) {\n                        // 确保两个列表大小一致\n                        if (displayPaths.size == originalPaths.size) {\n                            val lastDisplayItem = displayPaths.removeLast()\n                            val lastOriginalItem = originalPaths.removeLast()\n                            pathsUndone.add(Pair(lastDisplayItem, lastOriginalItem))\n                            \n                            // 清空当前绘制状态\n                            currentDrawingPath.value = null\n                            \n                            logger.info(\"撤销操作：移除了一个路径，当前路径数量: displayPaths=${displayPaths.size}, originalPaths=${originalPaths.size}\")\n                        } else {\n                            logger.warn(\"路径列表大小不一致: displayPaths=${displayPaths.size}, originalPaths=${originalPaths.size}\")\n                        }\n                    } else {\n                        logger.info(\"没有可撤销的操作: displayPaths=${displayPaths.size}, originalPaths=${originalPaths.size}\")\n                    }\n                })\n\n            // 撤回\n            toolTipButton(text = i18nState.get(\"revoke\"),\n                painter = painterResource(\"images/doodle/revoke.png\"),\n                onClick = {\n                    if (pathsUndone.isNotEmpty()) {\n                        val lastUndoPaths = pathsUndone.removeLast()\n                        val (displayPath, originalPath) = lastUndoPaths\n                        displayPaths.add(displayPath)\n                        originalPaths.add(originalPath)\n                        \n                        // 强制重绘：重置绘制状态\n                        drawingState.value = Triple(MotionEvent.Idle, Offset.Unspecified, Path())\n                        \n                        logger.info(\"重做操作：恢复了一个路径\")\n                    } else {\n                        logger.info(\"没有可重做的操作\")\n                    }\n                })\n\n            // 清空画布\n            toolTipButton(text = i18nState.get(\"clear_canvas\"),\n                painter = painterResource(\"images/doodle/clear.png\"),\n                onClick = {\n                    // 清空所有路径\n                    displayPaths.clear()\n                    originalPaths.clear()\n                    pathsUndone.clear()\n                    \n                    // 重置当前绘制状态\n                    currentDisplayPath = Path()\n                    currentOriginalPath = Path()\n                    currentPosition = Offset.Unspecified\n                    previousPosition = Offset.Unspecified\n                    motionEvent = MotionEvent.Idle\n                    \n                    // 重置绘制状态，强制重绘\n                    drawingState.value = Triple(MotionEvent.Idle, Offset.Unspecified, Path())\n                    \n                    logger.info(\"画布已清空，所有状态已重置\")\n                })\n\n            // 保存\n            toolTipButton(text = i18nState.get(\"save\"),\n                painter = painterResource(\"images/doodle/save.png\"),\n                onClick = {\n                    viewModel.saveCanvasToBitmap(density, originalPaths, image, state)\n                })\n        }\n\n        if (showColorDialog) {\n            ColorSelectionDialog(\n                currentPathProperty.color,\n                onDismiss = { showColorDialog = false },\n                onNegativeClick = { showColorDialog = false },\n                onPositiveClick = { color: Color ->\n                    showColorDialog = false\n                    currentPathProperty = currentPathProperty.copy(color = color)\n                    logger.info(\"颜色已更改: ${color}\")\n                    \n                    // 更新当前绘制路径的颜色\n                    currentDrawingPath.value?.let { (path, props) ->\n                        currentDrawingPath.value = Pair(path, props.copy(color = color))\n                    }\n                }\n            )\n        }\n\n        if (showPropertiesDialog) {\n            PropertiesMenuDialog(\n                pathOption = currentPathProperty, \n                onDismiss = {\n                    showPropertiesDialog = false\n                },\n                onPropertiesChanged = { updatedProperty ->\n                    currentPathProperty = updatedProperty\n                    showPropertiesDialog = false\n                    \n                    // 更新当前绘制路径的属性\n                    currentDrawingPath.value?.let { (path, props) ->\n                        currentDrawingPath.value = Pair(path, updatedProperty)\n                    }\n                },\n                title = i18nState.get(\"brush_settings\")\n            )\n        }\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/doodle/DoodleViewModel.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.doodle\n\nimport androidx.compose.ui.geometry.Size\nimport androidx.compose.ui.graphics.*\nimport androidx.compose.ui.graphics.drawscope.CanvasDrawScope\nimport androidx.compose.ui.graphics.drawscope.Stroke\nimport androidx.compose.ui.unit.Density\nimport androidx.compose.ui.unit.LayoutDirection\nimport androidx.compose.ui.unit.IntSize\nimport cn.netdiscovery.monica.state.ApplicationState\nimport cn.netdiscovery.monica.ui.controlpanel.doodle.model.PathProperties\nimport org.slf4j.Logger\nimport org.slf4j.LoggerFactory\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.doodle.DoodleViewModel\n * @author: Tony Shen\n * @date: 2024/5/25 20:49\n * @version: V1.0 <描述当前版本功能>\n */\nclass DoodleViewModel {\n    \n    private val logger: Logger = LoggerFactory.getLogger(DoodleViewModel::class.java)\n\n    fun saveCanvasToBitmap(\n        density: Density, \n        paths: List<Pair<Path, PathProperties>>, \n        image: ImageBitmap, \n        state: ApplicationState\n    ) {\n        logger.info(\"开始保存涂鸦到图片，路径数量: ${paths.size}\")\n        \n        val bitmapWidth = image.width\n        val bitmapHeight = image.height\n        \n        logger.info(\"原始图片尺寸: ${bitmapWidth}x${bitmapHeight}\")\n\n        val drawScope = CanvasDrawScope()\n        val size = Size(bitmapWidth.toFloat(), bitmapHeight.toFloat())\n        val canvas = Canvas(image)\n\n        drawScope.draw(\n            density = density,\n            layoutDirection = LayoutDirection.Ltr,\n            canvas = canvas,\n            size = size,\n        ) {\n            state.closePreviewWindow()\n\n            // 先绘制原始图片\n            drawImage(image = image, dstSize = IntSize(bitmapWidth, bitmapHeight))\n\n            // 直接绘制路径，因为现在路径已经是基于原始图片尺寸的\n            paths.forEach { pathPair ->\n                val path = pathPair.first\n                val property = pathPair.second\n                \n                drawPath(\n                    color = property.color,\n                    path = path,\n                    style = Stroke(\n                        width = property.strokeWidth,\n                        cap = property.strokeCap,\n                        join = property.strokeJoin\n                    )\n                )\n            }\n        }\n\n        state.addQueue(state.currentImage!!)\n        state.currentImage = image.toAwtImage()\n        logger.info(\"涂鸦保存完成\")\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/doodle/model/PathProperties.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.doodle.model\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.StrokeCap\nimport androidx.compose.ui.graphics.StrokeJoin\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.doodle.model.PathProperties\n * @author: Tony Shen\n * @date: 2024/6/14 15:57\n * @version: V1.0 <描述当前版本功能>\n */\ndata class PathProperties(\n    val strokeWidth: Float = 10f,\n    val color: Color = Color.Black,\n    val alpha: Float = 1f,\n    val strokeCap: StrokeCap = StrokeCap.Round,\n    val strokeJoin: StrokeJoin = StrokeJoin.Round\n)"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/doodle/widget/PropertiesMenuDialog.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.doodle.widget\n\nimport androidx.compose.foundation.Canvas\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.Button\nimport androidx.compose.material.Card\nimport androidx.compose.material.Slider\nimport androidx.compose.material.Text\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.graphics.Path\nimport androidx.compose.ui.graphics.StrokeCap\nimport androidx.compose.ui.graphics.StrokeJoin\nimport androidx.compose.ui.graphics.drawscope.Stroke\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport androidx.compose.ui.window.Dialog\nimport cn.netdiscovery.monica.ui.controlpanel.doodle.model.PathProperties\nimport cn.netdiscovery.monica.ui.widget.color.Blue400\nimport cn.netdiscovery.monica.ui.widget.properties.ExposedSelectionMenu\nimport cn.netdiscovery.monica.i18n.getCurrentStringResource\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.doodle.PropertiesMenuDialog\n * @author: Tony Shen\n * @date: 2024/6/16 12:38\n * @version: V1.0 <描述当前版本功能>\n */\n@Composable\nfun PropertiesMenuDialog(\n    pathOption: PathProperties, \n    onDismiss: () -> Unit,\n    onPropertiesChanged: (PathProperties) -> Unit = {},\n    title: String = \"Properties\"\n) {\n    val i18nState = getCurrentStringResource()\n    var strokeWidth by remember { mutableStateOf(pathOption.strokeWidth) }\n    var strokeCap by remember { mutableStateOf(pathOption.strokeCap) }\n    var strokeJoin by remember { mutableStateOf(pathOption.strokeJoin) }\n    val focusRequester = remember { FocusRequester() }\n\n    Dialog(onDismissRequest = onDismiss) {\n\n        Card(\n            elevation = 2.dp,\n            shape = RoundedCornerShape(8.dp),\n            modifier = Modifier\n                .padding(vertical = 8.dp)\n                .height(400.dp)\n        ) {\n            Column(modifier = Modifier.padding(8.dp)) {\n\n                Text(\n                    text = title,\n                    color = Blue400,\n                    fontSize = 18.sp,\n                    fontWeight = FontWeight.Bold,\n                    modifier = Modifier.padding(start = 12.dp, top = 12.dp)\n                )\n\n                Canvas(\n                    modifier = Modifier\n                        .padding(horizontal = 24.dp, vertical = 20.dp)\n                        .height(40.dp)\n                        .fillMaxWidth()\n                ) {\n                    val path = Path()\n                    path.moveTo(0f, size.height / 2)\n                    path.lineTo(size.width, size.height / 2)\n\n                    drawPath(\n                        color = pathOption.color,\n                        path = path,\n                        style = Stroke(\n                            width = strokeWidth,\n                            cap = strokeCap,\n                            join = strokeJoin\n                        )\n                    )\n                }\n\n                Text(\n                    text = i18nState.get(\"stroke_width\") + \" ${strokeWidth.toInt()}\",\n                    fontSize = 16.sp,\n                    modifier = Modifier.padding(horizontal = 12.dp)\n                )\n\n                Slider(\n                    value = strokeWidth,\n                    onValueChange = {\n                        strokeWidth = it\n                    },\n                    valueRange = 1f..100f,\n                    onValueChangeFinished = {},\n                    modifier = Modifier.padding(horizontal = 12.dp).focusRequester(focusRequester)\n                )\n\n                ExposedSelectionMenu(title = i18nState.get(\"stroke_cap\"),\n                    index = when (strokeCap) {\n                        StrokeCap.Butt -> 0\n                        StrokeCap.Round -> 1\n                        else -> 2\n                    },\n                    options = listOf(\"Butt\", \"Round\", \"Square\"),\n                    onSelected = {\n                        println(\"STOKE CAP $it\")\n                        strokeCap = when (it) {\n                            0 -> StrokeCap.Butt\n                            1 -> StrokeCap.Round\n                            else -> StrokeCap.Square\n                        }\n                    }\n                )\n\n                ExposedSelectionMenu(title = i18nState.get(\"stroke_join\"),\n                    index = when (strokeJoin) {\n                        StrokeJoin.Miter -> 0\n                        StrokeJoin.Round -> 1\n                        else -> 2\n                    },\n                    options = listOf(\"Miter\", \"Round\", \"Bevel\"),\n                    onSelected = {\n                        println(\"STOKE JOIN $it\")\n\n                        strokeJoin = when (it) {\n                            0 -> StrokeJoin.Miter\n                            1 -> StrokeJoin.Round\n                            else -> StrokeJoin.Bevel\n                        }\n                    }\n                )\n                \n                // 添加确认和取消按钮\n                Row(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .padding(horizontal = 12.dp, vertical = 8.dp)\n                ) {\n                    Button(\n                        onClick = {\n                            println(\"确认按钮被点击\")\n                            onPropertiesChanged(\n                                PathProperties(\n                                    strokeWidth = strokeWidth,\n                                    color = pathOption.color,\n                                    alpha = pathOption.alpha,\n                                    strokeCap = strokeCap,\n                                    strokeJoin = strokeJoin\n                                )\n                            )\n                            onDismiss()\n                        },\n                        modifier = Modifier.weight(1f)\n                    ) {\n                        Text(i18nState.get(\"confirm\"))\n                    }\n                    \n                    Button(\n                        onClick = {\n                            println(\"取消按钮被点击\")\n                            onDismiss()\n                        },\n                        modifier = Modifier.weight(1f)\n                    ) {\n                        Text(i18nState.get(\"cancel\"))\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/filter/FilterView.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.filter\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.unit.dp\nimport cn.netdiscovery.monica.state.ApplicationState\nimport cn.netdiscovery.monica.ui.controlpanel.filter.viewmodel.FilterViewModel\nimport cn.netdiscovery.monica.ui.controlpanel.filter.widget.FilterAdjustmentPanel\nimport cn.netdiscovery.monica.ui.controlpanel.filter.widget.FilterListPanel\nimport cn.netdiscovery.monica.ui.controlpanel.filter.widget.FilterPreviewArea\nimport cn.netdiscovery.monica.ui.controlpanel.filter.widget.FilterTopAppBar\nimport cn.netdiscovery.monica.ui.controlpanel.filter.widget.buildDefaultParamMap\nimport cn.netdiscovery.monica.ui.i18n.rememberI18nState\nimport cn.netdiscovery.monica.ui.widget.*\nimport cn.netdiscovery.monica.utils.chooseImage\nimport cn.netdiscovery.monica.utils.getBufferedImage\nimport filterNames\nimport loadingDisplay\nimport org.koin.compose.koinInject\nimport org.slf4j.Logger\nimport org.slf4j.LoggerFactory\nimport kotlin.collections.HashMap\n\n/**\n * 重构后的滤镜模块 UI\n * \n * @author: Tony Shen\n * @date: 2025/12/07\n * @version: V2.0\n */\nprivate val logger: Logger = LoggerFactory.getLogger(object : Any() {}.javaClass.enclosingClass)\n\n@Composable\nfun filter(state: ApplicationState) {\n    val i18nState = rememberI18nState()\n    val viewModel: FilterViewModel = koinInject()\n\n    // Toast 状态\n    var showToast by remember { mutableStateOf(false) }\n    var toastMessage by remember { mutableStateOf(\"\") }\n\n    // 搜索状态\n    var searchQuery by remember { mutableStateOf(\"\") }\n\n    // 预览图像状态（用于实时预览）\n    var previewImage by remember { mutableStateOf<java.awt.image.BufferedImage?>(null) }\n    var isDirty by remember { mutableStateOf(false) } // 是否有未应用的更改\n    var paramVersion by remember { mutableStateOf(0) } // 用于强制刷新参数控件状态\n    var appliedParamSnapshot by remember { mutableStateOf<Map<Pair<String, String>, String>>(emptyMap()) } // 上次 Apply 的参数快照\n    val selectedIndexState = remember { mutableStateOf(-1) }\n    val paramMap = remember { androidx.compose.runtime.mutableStateMapOf<Pair<String, String>, String>() } // 当前参数（UI 状态源）\n    // 进入滤镜模块前的基线图：用于“清除滤镜”恢复原效果\n    val baseImageSnapshot = remember { mutableStateOf<java.awt.image.BufferedImage?>(null) }\n    // 当前选中滤镜的基线图：用于“同一滤镜内调参不叠加”，但允许滤镜之间叠加\n    val currentFilterBaseImageSnapshot = remember { mutableStateOf<java.awt.image.BufferedImage?>(null) }\n\n    // 打开滤镜模块时，锁定一次基线（仅首次）；后续如用户重新加载图片，会在 onImageClick 中更新\n    LaunchedEffect(Unit) {\n        if (baseImageSnapshot.value == null) {\n            // 基线要以“进入滤镜模块前的效果”为准，所以优先 currentImage\n            baseImageSnapshot.value = state.currentImage ?: state.rawImage\n        }\n    }\n\n    // 缩放状态\n    var zoomLevel by remember { mutableStateOf(1.0f) }\n\n    PageLifecycle(\n        onInit = {\n            logger.info(\"FilterView 启动时初始化\")\n        },\n        onDisposeEffect = {\n            logger.info(\"FilterView 关闭时释放资源\")\n            viewModel.clear()\n        }\n    )\n\n    Column(\n        modifier = Modifier\n            .fillMaxSize()\n            .background(Color(0xFFF5F5F5))\n    ) {\n        // Top App Bar\n        FilterTopAppBar(\n            onSave = {\n                state.closePreviewWindow()\n            },\n            onExport = {\n                // TODO: 实现导出功能\n            },\n            i18nState = i18nState\n        )\n\n        // Main Content Area\n        Row(\n            modifier = Modifier\n                .fillMaxSize()\n                .weight(1f),\n            horizontalArrangement = Arrangement.spacedBy(0.dp)\n        ) {\n            // Left Sidebar - Filter List\n            FilterListPanel(\n                modifier = Modifier.width(280.dp),\n                searchQuery = searchQuery,\n                onSearchQueryChange = { searchQuery = it },\n                selectedIndex = selectedIndexState.value,\n                onFilterSelected = { index ->\n                    selectedIndexState.value = index\n                    isDirty = false\n                    previewImage = null\n                    // 重置参数为默认值\n                    paramMap.clear()\n                    val filterName = filterNames[index]\n                    paramMap.putAll(buildDefaultParamMap(filterName))\n                    // 切换滤镜时：默认参数也作为“已应用”的基线（直到用户 Apply）\n                    appliedParamSnapshot = HashMap(paramMap)\n                    paramVersion++\n                    // 方式1：滤镜之间叠加 —— 新滤镜基于当前画布图像继续算\n                    val base = state.currentImage ?: state.rawImage\n                    if (base != null) {\n                        currentFilterBaseImageSnapshot.value = base\n                        viewModel.applyFilter(\n                            state = state,\n                            index = index,\n                            paramMap = HashMap(paramMap),\n                            sourceImage = base,\n                            pushHistory = true\n                        )\n                    }\n                },\n                state = state,\n                i18nState = i18nState\n            )\n\n            // Center - Image Preview Area\n            FilterPreviewArea(\n                modifier = Modifier.weight(1f),\n                state = state,\n                previewImage = previewImage,\n                zoomLevel = zoomLevel,\n                onZoomChange = { zoomLevel = it },\n                onImageClick = {\n                    if (state.currentImage == null) {\n                        chooseImage(state) { file ->\n                            state.rawImage = getBufferedImage(file, state)\n                            state.currentImage = state.rawImage\n                            state.rawImageFile = file\n                            baseImageSnapshot.value = state.currentImage\n                            currentFilterBaseImageSnapshot.value = state.currentImage\n                            previewImage = null\n                            isDirty = false\n                        }\n                    }\n                },\n                i18nState = i18nState\n            )\n\n            // Right Sidebar - Adjustment Panel\n            FilterAdjustmentPanel(\n                modifier = Modifier.width(300.dp),\n                selectedIndex = selectedIndexState.value,\n                state = state,\n                filterBaseImage = currentFilterBaseImageSnapshot.value,\n                viewModel = viewModel,\n                previewImage = previewImage,\n                onPreviewImageChange = { previewImage = it },\n                isDirty = isDirty,\n                onDirtyChange = { isDirty = it },\n                paramVersion = paramVersion,\n                onParamVersionChange = { paramVersion = it },\n                appliedParamSnapshot = appliedParamSnapshot,\n                onAppliedParamSnapshotChange = { appliedParamSnapshot = it },\n                paramMap = paramMap,\n                onClearFilter = {\n                    val base = baseImageSnapshot.value ?: return@FilterAdjustmentPanel\n                    val before = state.currentImage ?: base\n                    // 回到进入滤镜模块前的效果，并记录一次历史便于撤销\n                    state.currentImage = base\n                    state.addQueue(before)\n                    // 清理 UI 状态：取消选中滤镜，避免“选中但未应用”的错觉\n                    selectedIndexState.value = -1\n                    paramMap.clear()\n                    appliedParamSnapshot = emptyMap()\n                    currentFilterBaseImageSnapshot.value = null\n                    previewImage = null\n                    isDirty = false\n                    paramVersion++\n                },\n                onShowToast = { message ->\n                    toastMessage = message\n                    showToast = true\n                },\n                i18nState = i18nState\n            )\n        }\n    }\n\n    // Loading Indicator\n    if (loadingDisplay) {\n        showLoading()\n    }\n\n    // Toast Message\n    if (showToast) {\n        centerToast(\n            modifier = Modifier,\n            message = toastMessage\n        ) {\n            showToast = false\n        }\n    }\n}\n"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/filter/viewmodel/FilterViewModel.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.filter.viewmodel\n\nimport cn.netdiscovery.monica.config.storage.ConfigManager\nimport cn.netdiscovery.monica.config.storage.ConfigType\nimport cn.netdiscovery.monica.rxcache.FilterParam\nimport cn.netdiscovery.monica.rxcache.Param\nimport cn.netdiscovery.monica.state.ApplicationState\nimport cn.netdiscovery.monica.utils.collator\nimport cn.netdiscovery.monica.utils.doFilter\nimport cn.netdiscovery.monica.utils.extensions.launchWithSuspendLoading\nimport cn.netdiscovery.monica.utils.extensions.safelyConvertToInt\nimport cn.netdiscovery.monica.utils.logger\nimport filterNames\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.launch\nimport org.slf4j.Logger\nimport java.awt.image.BufferedImage\nimport java.util.LinkedHashMap\nimport kotlin.math.max\nimport kotlin.math.min\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.filter.FilterViewModel\n * @author: Tony Shen\n * @date: 2024/5/8 12:09\n * @version: V1.0 <描述当前版本功能>\n */\n\nclass FilterViewModel {\n\n    private val logger: Logger = logger<FilterViewModel>()\n    var job: Job? = null\n    var previewJob: Job? = null\n\n    private data class PreviewCacheKey(\n        val baseImageId: Int,\n        val filterName: String,\n        val paramsHash: Int\n    )\n\n    private data class CachedPreview(\n        val image: BufferedImage,\n        val approxBytes: Long\n    )\n\n    private val previewCacheLock = Any()\n    private val previewCacheMaxEntries = 80\n    private val previewCacheMaxBytes = 256L * 1024L * 1024L // 256MB\n    private var previewCacheBytes: Long = 0\n    private val previewCache: LinkedHashMap<PreviewCacheKey, CachedPreview> =\n        object : LinkedHashMap<PreviewCacheKey, CachedPreview>(64, 0.75f, true) {\n            override fun removeEldestEntry(eldest: MutableMap.MutableEntry<PreviewCacheKey, CachedPreview>?): Boolean {\n                return size > previewCacheMaxEntries\n            }\n        }\n\n    private fun approxImageBytes(image: BufferedImage): Long {\n        // 估算：每像素 4 bytes，做上限保护防止溢出\n        val pixels = image.width.toLong() * image.height.toLong()\n        return min(pixels * 4L, Long.MAX_VALUE / 4L)\n    }\n\n    private fun buildParamsHash(paramMap: Map<Pair<String, String>, String>): Int {\n        // 需要稳定：排序后 hash\n        val entries = paramMap.entries\n            .sortedWith(compareBy({ it.key.first.lowercase() }, { it.key.second }, { it.value }))\n        var acc = 1\n        for (e in entries) {\n            acc = 31 * acc + e.key.first.hashCode()\n            acc = 31 * acc + e.key.second.hashCode()\n            acc = 31 * acc + e.value.hashCode()\n        }\n        return acc\n    }\n\n    private fun clampIntParam(key: String, value: Int): Int {\n        // 兜底：某些滤镜会把参数用作 step，必须 >0\n        return when (key) {\n            \"blockSize\" -> max(1, value)\n            else -> value\n        }\n    }\n\n    private fun getCachedPreview(key: PreviewCacheKey): BufferedImage? {\n        synchronized(previewCacheLock) {\n            return previewCache[key]?.image\n        }\n    }\n\n    private fun putCachedPreview(key: PreviewCacheKey, image: BufferedImage) {\n        val bytes = approxImageBytes(image)\n        synchronized(previewCacheLock) {\n            // 若已有旧值，先扣掉\n            previewCache.remove(key)?.let { old ->\n                previewCacheBytes -= old.approxBytes\n            }\n            previewCache[key] = CachedPreview(image = image, approxBytes = bytes)\n            previewCacheBytes += bytes\n\n            // 先按 entry 数做一次淘汰（LinkedHashMap 自带）\n            while (previewCache.size > previewCacheMaxEntries) {\n                val eldestKey = previewCache.entries.firstOrNull()?.key ?: break\n                previewCache.remove(eldestKey)?.let { removed ->\n                    previewCacheBytes -= removed.approxBytes\n                }\n            }\n\n            // 再按内存上限做淘汰\n            while (previewCacheBytes > previewCacheMaxBytes && previewCache.isNotEmpty()) {\n                val eldestKey = previewCache.entries.firstOrNull()?.key ?: break\n                previewCache.remove(eldestKey)?.let { removed ->\n                    previewCacheBytes -= removed.approxBytes\n                }\n            }\n        }\n    }\n\n    /**\n     * 保存滤镜参数，并调用滤镜效果\n     */\n    fun applyFilter(\n        state: ApplicationState,\n        index: Int,\n        paramMap: Map<Pair<String, String>, String>,\n        sourceImage: BufferedImage? = null,\n        pushHistory: Boolean = true\n    ) {\n        job = state.scope.launchWithSuspendLoading {\n            val baseImage = sourceImage ?: state.rawImage ?: state.currentImage\n            if (baseImage == null) return@launchWithSuspendLoading\n            val tempImage = state.currentImage ?: baseImage\n\n            val filterName = filterNames[index]\n\n            val list = mutableListOf<Param>()\n            paramMap.forEach { (t, u) ->\n                val value = when(t.second) {\n                    \"Int\"    -> clampIntParam(t.first, u.safelyConvertToInt() ?: 0)\n                    \"Float\"  -> u.toFloat()\n                    \"Double\" -> u.toDouble()\n                    else     -> u\n                }\n\n                list.add(Param(t.first, t.second, value))\n            }\n\n            // 按照参数名首字母进行排序\n            list.sortWith { o1, o2 -> collator.compare(o1.key, o2.key); }\n\n            // 加载滤镜参数配置，更新参数列表并保存\n            val defaultFilterParam = FilterParam(filterName, null, null, emptyList())\n            val filterParam = ConfigManager.load(filterName, defaultFilterParam, ConfigType.RX_CACHE)\n            filterParam.params = list\n            ConfigManager.save(filterName, filterParam, ConfigType.RX_CACHE) // 保存滤镜参数\n\n            val array:MutableList<Any> = list.map { it.value }.toMutableList()\n            logger.info(\"filterName: $filterName, array: $array\")\n\n            state.currentImage = doFilter(\n                filterName = filterName,\n                array = array,\n                image = baseImage\n            )\n\n            if (pushHistory) {\n                state.addQueue(tempImage)\n            }\n        }\n    }\n\n    /**\n     * 应用滤镜预览（不保存到历史记录，用于实时预览）\n     */\n    fun applyFilterPreview(\n        state: ApplicationState,\n        index: Int,\n        paramMap: Map<Pair<String, String>, String>,\n        sourceImageOverride: BufferedImage? = null,\n        debounceMs: Long = 0,\n        onSuccess: (BufferedImage) -> Unit,\n        onError: (Throwable) -> Unit\n    ) {\n        // 取消之前的预览任务\n        previewJob?.cancel()\n\n        previewJob = state.scope.launch {\n            try {\n                if (debounceMs > 0) {\n                    delay(debounceMs)\n                }\n                // 预览基线：优先用外部传入（用于“滤镜叠加但单滤镜内不叠加”），否则用 currentImage\n                val sourceImage = sourceImageOverride ?: state.currentImage ?: state.rawImage\n                if (sourceImage == null) {\n                    return@launch\n                }\n\n                val filterName = filterNames[index]\n\n                // Preview cache：相同滤镜 + 相同参数 + 相同基线图 -> 命中\n                val cacheKey = PreviewCacheKey(\n                    baseImageId = System.identityHashCode(sourceImage),\n                    filterName = filterName,\n                    paramsHash = buildParamsHash(paramMap)\n                )\n                getCachedPreview(cacheKey)?.let { cached ->\n                    onSuccess(cached)\n                    return@launch\n                }\n\n                val list = mutableListOf<Param>()\n                paramMap.forEach { (t, u) ->\n                    val value = when(t.second) {\n                        \"Int\"    -> clampIntParam(t.first, u.safelyConvertToInt() ?: 0)\n                        \"Float\"  -> u.toFloat()\n                        \"Double\" -> u.toDouble()\n                        else     -> u\n                    }\n\n                    list.add(Param(t.first, t.second, value))\n                }\n\n                // 按照参数名首字母进行排序\n                list.sortWith { o1, o2 -> collator.compare(o1.key, o2.key); }\n\n                val array: MutableList<Any> = list.map { it.value }.toMutableList()\n\n                // 预览只处理图像，不触碰 state.currentImage，避免 UI 闪烁/并发风险\n                val previewResult = doFilter(\n                    filterName = filterName,\n                    array = array,\n                    image = sourceImage\n                )\n                putCachedPreview(cacheKey, previewResult)\n                onSuccess(previewResult)\n            } catch (e: Exception) {\n                logger.error(\"Preview filter failed\", e)\n                onError(e)\n            }\n        }\n    }\n\n    fun clear() {\n        if (job !=null && !job!!.isCancelled) {\n            job?.cancel()\n        }\n        if (previewJob !=null && !previewJob!!.isCancelled) {\n            previewJob?.cancel()\n        }\n        synchronized(previewCacheLock) {\n            previewCache.clear()\n            previewCacheBytes = 0\n        }\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/filter/widget/FilterAdjustmentPanel.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.filter.widget\n\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.text.KeyboardActions\nimport androidx.compose.foundation.text.KeyboardOptions\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material.icons.filled.ArrowDropDown\nimport androidx.compose.material.*\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.KeyboardArrowDown\nimport androidx.compose.material.icons.filled.KeyboardArrowUp\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.platform.LocalFocusManager\nimport androidx.compose.ui.focus.onFocusChanged\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.input.ImeAction\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport cn.netdiscovery.monica.rxcache.Param\nimport cn.netdiscovery.monica.rxcache.getFilterParam\nimport cn.netdiscovery.monica.rxcache.getFilterRemark\nimport cn.netdiscovery.monica.state.ApplicationState\nimport cn.netdiscovery.monica.ui.controlpanel.filter.viewmodel.FilterViewModel\nimport cn.netdiscovery.monica.ui.i18n.I18nState\nimport cn.netdiscovery.monica.utils.collator\nimport cn.netdiscovery.monica.utils.extensions.safelyConvertToInt\nimport filterNames\nimport kotlinx.coroutines.isActive\nimport kotlinx.coroutines.delay\nimport java.awt.image.BufferedImage\nimport java.util.*\nimport kotlin.math.roundToInt\n\n/**\n * 右侧参数调整面板\n */\n@Composable\nfun FilterAdjustmentPanel(\n    modifier: Modifier = Modifier,\n    selectedIndex: Int,\n    state: ApplicationState,\n    filterBaseImage: BufferedImage?,\n    viewModel: FilterViewModel,\n    previewImage: BufferedImage?,\n    onPreviewImageChange: (BufferedImage?) -> Unit,\n    isDirty: Boolean,\n    onDirtyChange: (Boolean) -> Unit,\n    paramVersion: Int,\n    onParamVersionChange: (Int) -> Unit,\n    appliedParamSnapshot: Map<Pair<String, String>, String>,\n    onAppliedParamSnapshotChange: (Map<Pair<String, String>, String>) -> Unit,\n    paramMap: MutableMap<Pair<String, String>, String>,\n    onClearFilter: () -> Unit,\n    onShowToast: (String) -> Unit,\n    i18nState: I18nState\n) {\n    var expanded by remember(selectedIndex) { mutableStateOf(true) }\n    Surface(\n        modifier = modifier.fillMaxHeight(),\n        color = Color.White,\n        elevation = 1.dp\n    ) {\n        Column(\n            modifier = Modifier.fillMaxSize()\n        ) {\n            // 面板标题\n            Surface(\n                modifier = Modifier.fillMaxWidth(),\n                color = Color(0xFFF5F5F5),\n                elevation = 1.dp\n            ) {\n                Text(\n                    text = i18nState.getString(\"adjustments\"),\n                    fontSize = 18.sp,\n                    fontWeight = FontWeight.Bold,\n                    color = Color(0xFF222222),\n                    modifier = Modifier.padding(20.dp)\n                )\n            }\n            \n            // 可滚动内容区域\n            Column(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .weight(1f)\n                    .verticalScroll(rememberScrollState())\n                    .padding(20.dp),\n                verticalArrangement = Arrangement.spacedBy(16.dp)\n            ) {\n                if (selectedIndex >= 0) {\n                    val filterName = filterNames[selectedIndex]\n                    \n                    // 滤镜名称（可折叠区域）\n                    FilterNameSection(\n                        filterName = filterName,\n                        expanded = expanded,\n                        onExpandedChange = { expanded = it }\n                    )\n\n                    // 收起时：展示参数摘要 + Reset 提示，让用户一眼理解当前状态\n                    if (!expanded) {\n                        FilterParamSummarySection(\n                            filterName = filterName,\n                            paramMap = paramMap,\n                            i18nState = i18nState\n                        )\n                    }\n                    \n                    AnimatedVisibility(visible = expanded) {\n                        Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {\n                            // 参数滑块\n                            FilterParamsSection(\n                                filterName = filterName,\n                                selectedIndex = selectedIndex,\n                                state = state,\n                                filterBaseImage = filterBaseImage,\n                                viewModel = viewModel,\n                                onPreviewImageChange = onPreviewImageChange,\n                                onDirtyChange = onDirtyChange,\n                                paramVersion = paramVersion,\n                                paramMap = paramMap,\n                                onCommitted = { latest ->\n                                    onAppliedParamSnapshotChange(latest)\n                                },\n                                i18nState = i18nState\n                            )\n\n                            // Notes 区域\n                            FilterNotesSection(\n                                filterName = filterName,\n                                i18nState = i18nState\n                            )\n                        }\n                    }\n                } else {\n                    Text(\n                        text = i18nState.getString(\"select_filter_first\"),\n                        fontSize = 14.sp,\n                        color = Color(0xFF666666),\n                        modifier = Modifier.padding(vertical = 20.dp)\n                    )\n                }\n            }\n            \n            // 底部按钮区域\n            if (selectedIndex >= 0) {\n                Divider(color = Color(0xFFE0E0E0), thickness = 1.dp)\n                FilterActionButtons(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .padding(20.dp),\n                    state = state,\n                    filterBaseImage = filterBaseImage,\n                    viewModel = viewModel,\n                    selectedIndex = selectedIndex,\n                    previewImage = previewImage,\n                    onPreviewImageChange = onPreviewImageChange,\n                    isDirty = isDirty,\n                    onDirtyChange = onDirtyChange,\n                    paramVersion = paramVersion,\n                    onParamVersionChange = onParamVersionChange,\n                    appliedParamSnapshot = appliedParamSnapshot,\n                    onAppliedParamSnapshotChange = onAppliedParamSnapshotChange,\n                    paramMap = paramMap,\n                    onClearFilter = onClearFilter,\n                    onShowToast = onShowToast,\n                    i18nState = i18nState\n                )\n            }\n        }\n    }\n}\n\n/**\n * 滤镜名称区域（可折叠）\n */\n@Composable\nprivate fun FilterNameSection(\n    filterName: String,\n    expanded: Boolean,\n    onExpandedChange: (Boolean) -> Unit\n) {\n    Card(\n        modifier = Modifier.fillMaxWidth(),\n        elevation = 2.dp,\n        shape = RoundedCornerShape(8.dp),\n        backgroundColor = Color.White\n    ) {\n        Column {\n            Row(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .clickable { onExpandedChange(!expanded) }\n                    .padding(16.dp),\n                horizontalArrangement = Arrangement.SpaceBetween,\n                verticalAlignment = Alignment.CenterVertically\n            ) {\n                Text(\n                    text = filterName,\n                    fontSize = 16.sp,\n                    fontWeight = FontWeight.Bold,\n                    color = Color(0xFF222222)\n                )\n                Icon(\n                    imageVector = if (expanded) Icons.Default.KeyboardArrowUp else Icons.Default.KeyboardArrowDown,\n                    contentDescription = null,\n                    tint = Color(0xFF666666)\n                )\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun FilterParamSummarySection(\n    filterName: String,\n    paramMap: Map<Pair<String, String>, String>,\n    i18nState: I18nState\n) {\n    val defaultMap = remember(filterName) { buildDefaultParamMap(filterName) }\n    val paramByKey = remember(filterName) {\n        getFilterParam(filterName).orEmpty().associateBy { it.key }\n    }\n    val changedEntries = remember(filterName, defaultMap, paramMap) {\n        paramMap.entries\n            .filter { (k, v) -> defaultMap[k] != v }\n            .sortedWith(compareBy({ it.key.first.lowercase() }, { it.key.second }))\n    }\n\n    Card(\n        modifier = Modifier.fillMaxWidth(),\n        elevation = 1.dp,\n        shape = RoundedCornerShape(8.dp),\n        backgroundColor = Color(0xFFFAFAFA)\n    ) {\n        Column(\n            modifier = Modifier.padding(16.dp),\n            verticalArrangement = Arrangement.spacedBy(8.dp)\n        ) {\n            Row(\n                modifier = Modifier.fillMaxWidth(),\n                horizontalArrangement = Arrangement.SpaceBetween,\n                verticalAlignment = Alignment.CenterVertically\n            ) {\n                Text(\n                    text = i18nState.getString(\"param_summary\"),\n                    fontSize = 14.sp,\n                    fontWeight = FontWeight.Bold,\n                    color = Color(0xFF222222)\n                )\n\n                if (changedEntries.isEmpty()) {\n                    Text(\n                        text = i18nState.getString(\"param_summary_default\"),\n                        fontSize = 12.sp,\n                        color = Color(0xFF666666)\n                    )\n                } else {\n                    Text(\n                        text = i18nState.getString(\"param_summary_changed_count\").format(changedEntries.size),\n                        fontSize = 12.sp,\n                        color = Color(0xFF007AFF),\n                        fontWeight = FontWeight.Medium\n                    )\n                }\n            }\n\n            if (changedEntries.isNotEmpty()) {\n                val previewItems = changedEntries.take(3)\n                previewItems.forEach { entry ->\n                    val key = entry.key.first\n                    val rawValue = entry.value\n                    val value = run {\n                        val param = paramByKey[key]\n                        if (param != null) {\n                            val meta = FilterParamMetaRegistry.resolve(filterName = filterName, param = param)\n                            val intVal = rawValue.safelyConvertToInt()\n                            val option = intVal?.let { v -> meta.enumOptions?.firstOrNull { it.value == v } }\n                            if (option != null) {\n                                \"${i18nState.getString(option.labelKey)} ($rawValue)\"\n                            } else {\n                                rawValue\n                            }\n                        } else {\n                            rawValue\n                        }\n                    }\n                    Text(\n                        text = \"$key: $value\",\n                        fontSize = 12.sp,\n                        color = Color(0xFF444444),\n                        maxLines = 1\n                    )\n                }\n                if (changedEntries.size > 3) {\n                    Text(\n                        text = \"+${changedEntries.size - 3} ...\",\n                        fontSize = 12.sp,\n                        color = Color(0xFF666666)\n                    )\n                }\n\n                Text(\n                    text = i18nState.getString(\"param_summary_reset_hint\"),\n                    fontSize = 12.sp,\n                    color = Color(0xFF666666),\n                    lineHeight = 16.sp\n                )\n            }\n        }\n    }\n}\n\n/**\n * 滤镜参数区域\n */\n@Composable\nprivate fun FilterParamsSection(\n    filterName: String,\n    selectedIndex: Int,\n    state: ApplicationState,\n    filterBaseImage: BufferedImage?,\n    viewModel: FilterViewModel,\n    onPreviewImageChange: (BufferedImage?) -> Unit,\n    onDirtyChange: (Boolean) -> Unit,\n    paramVersion: Int,\n    paramMap: MutableMap<Pair<String, String>, String>,\n    onCommitted: (Map<Pair<String, String>, String>) -> Unit,\n    i18nState: I18nState\n) {\n    val params: List<Param>? = getFilterParam(filterName)\n    \n    if (params != null && params.isNotEmpty()) {\n        val sortedParams = remember(params) {\n            params.sortedWith { o1, o2 -> collator.compare(o1.key, o2.key) }\n        }\n        \n        sortedParams.forEach { param ->\n            FilterParamSlider(\n                param = param,\n                filterName = filterName,\n                selectedIndex = selectedIndex,\n                state = state,\n                filterBaseImage = filterBaseImage,\n                viewModel = viewModel,\n                onPreviewImageChange = onPreviewImageChange,\n                onDirtyChange = onDirtyChange,\n                paramVersion = paramVersion,\n                paramMap = paramMap,\n                onCommitted = onCommitted,\n                i18nState = i18nState\n            )\n        }\n    }\n}\n\n/**\n * 单个参数滑块\n */\n@Composable\nprivate fun FilterParamSlider(\n    param: Param,\n    filterName: String,\n    selectedIndex: Int,\n    state: ApplicationState,\n    filterBaseImage: BufferedImage?,\n    viewModel: FilterViewModel,\n    onPreviewImageChange: (BufferedImage?) -> Unit,\n    onDirtyChange: (Boolean) -> Unit,\n    paramVersion: Int,\n    paramMap: MutableMap<Pair<String, String>, String>,\n    onCommitted: (Map<Pair<String, String>, String>) -> Unit,\n    i18nState: I18nState\n) {\n    val paramKey = param.key\n    val paramType = param.type\n    val focusManager = LocalFocusManager.current\n    \n    // 从 filterTempMap 获取当前值，如果没有则使用默认值\n    val defaultValue = when (paramType) {\n        \"Int\" -> (param.value.toString().safelyConvertToInt() ?: 0).toString()\n        else -> param.value.toString()\n    }\n    \n    val initialValue = paramMap[Pair(paramKey, paramType)] ?: defaultValue\n    var draftText by remember(filterName, paramKey, paramVersion) { mutableStateOf(initialValue) }\n    var lastValidText by remember(filterName, paramKey, paramVersion) { mutableStateOf(initialValue) }\n    var hasFocus by remember { mutableStateOf(false) }\n    var isDragging by remember(filterName, paramKey, paramVersion) { mutableStateOf(false) }\n    var pendingSamplePreview by remember(filterName, paramKey, paramVersion) { mutableStateOf(false) }\n    \n    // 转换为数值用于滑块\n    val parsedValueOrNull: Float? = when (paramType) {\n        \"Int\" -> draftText.safelyConvertToInt()?.toFloat()\n        \"Float\" -> draftText.toFloatOrNull()\n        \"Double\" -> draftText.toDoubleOrNull()?.toFloat()\n        else -> null\n    }\n    val lastValidNumeric: Float = when (paramType) {\n        \"Int\" -> lastValidText.safelyConvertToInt()?.toFloat() ?: 0f\n        \"Float\" -> lastValidText.toFloatOrNull() ?: 0f\n        \"Double\" -> lastValidText.toDoubleOrNull()?.toFloat() ?: 0f\n        else -> 0f\n    }\n    val numericValue = parsedValueOrNull ?: lastValidNumeric\n    \n    val meta = remember(filterName, paramKey, paramType) {\n        FilterParamMetaRegistry.resolve(filterName = filterName, param = param)\n    }\n    val minValue = meta.min\n    val maxValue = meta.max\n    val step = meta.step\n    val decimals = meta.decimals\n    val enumOptions = meta.enumOptions\n\n    fun triggerPreviewNow() {\n        if (state.currentImage != null) {\n            viewModel.applyFilterPreview(\n                state = state,\n                index = selectedIndex,\n                paramMap = HashMap(paramMap),\n                sourceImageOverride = filterBaseImage,\n                debounceMs = 0,\n                onSuccess = { image -> onPreviewImageChange(image) },\n                onError = { }\n            )\n        }\n    }\n\n    fun commitNow() {\n        val base = filterBaseImage ?: state.currentImage ?: state.rawImage\n        if (base == null) return\n        viewModel.applyFilter(\n            state = state,\n            index = selectedIndex,\n            paramMap = HashMap(paramMap),\n            sourceImage = base,\n            pushHistory = true\n        )\n        onPreviewImageChange(null)\n        onDirtyChange(false)\n        onCommitted(HashMap(paramMap))\n    }\n\n    // Slider：拖动中不实时算，但每 300ms 抽样触发一次（仅当这段时间内有变化）\n    LaunchedEffect(filterName, paramKey, paramVersion, isDragging) {\n        if (!isDragging) return@LaunchedEffect\n        while (isActive && isDragging) {\n            delay(300)\n            if (pendingSamplePreview) {\n                pendingSamplePreview = false\n                triggerPreviewNow()\n            }\n        }\n    }\n    \n    Card(\n        modifier = Modifier\n            .fillMaxWidth()\n            .padding(vertical = 4.dp),\n        elevation = 1.dp,\n        shape = RoundedCornerShape(8.dp),\n        backgroundColor = Color.White\n    ) {\n        Column(\n            modifier = Modifier.padding(16.dp),\n            verticalArrangement = Arrangement.spacedBy(8.dp)\n        ) {\n            // 参数名称和数值显示\n            Row(\n                modifier = Modifier.fillMaxWidth(),\n                horizontalArrangement = Arrangement.SpaceBetween,\n                verticalAlignment = Alignment.CenterVertically\n            ) {\n                Text(\n                    text = paramKey,\n                    fontSize = 14.sp,\n                    fontWeight = FontWeight.Bold,\n                    color = Color(0xFF222222)\n                )\n                if (paramType == \"Int\" && !enumOptions.isNullOrEmpty()) {\n                    EnumParamDropdown(\n                        valueText = draftText,\n                        options = enumOptions,\n                        onSelect = { newInt ->\n                            val newValue = newInt.toString()\n                            draftText = newValue\n                            lastValidText = newValue\n                            paramMap[Pair(paramKey, paramType)] = newValue\n                            onDirtyChange(true)\n                            commitNow()\n                        },\n                        i18nState = i18nState\n                    )\n                } else {\n                    // 数字输入框\n                    OutlinedTextField(\n                        value = draftText,\n                        onValueChange = { newValue ->\n                            draftText = newValue\n\n                            // 仅当输入可解析时才更新参数与触发预览；否则等待失焦回退\n                            val ok = when (paramType) {\n                                \"Int\" -> newValue.safelyConvertToInt() != null\n                                \"Float\" -> newValue.toFloatOrNull() != null\n                                \"Double\" -> newValue.toDoubleOrNull() != null\n                                else -> true\n                            }\n\n                            if (ok) {\n                                lastValidText = newValue\n                                paramMap[Pair(paramKey, paramType)] = newValue\n                                onDirtyChange(true)\n                                // 输入过程中仍做预览（防止每个字符都提交）\n                                if (state.currentImage != null) {\n                                    viewModel.applyFilterPreview(\n                                        state = state,\n                                        index = selectedIndex,\n                                        paramMap = HashMap(paramMap),\n                                    sourceImageOverride = filterBaseImage,\n                                        debounceMs = 200,\n                                        onSuccess = { image -> onPreviewImageChange(image) },\n                                        onError = { }\n                                    )\n                                }\n                            }\n                        },\n                        modifier = Modifier\n                            .width(140.dp)\n                            .onFocusChanged { hasFocus = it.isFocused },\n                        singleLine = true,\n                        keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Done),\n                        keyboardActions = KeyboardActions(\n                            onDone = {\n                                focusManager.clearFocus()\n                                // Done：直接提交\n                                commitNow()\n                            }\n                        ),\n                        colors = TextFieldDefaults.outlinedTextFieldColors(\n                            focusedBorderColor = Color(0xFF007AFF),\n                            unfocusedBorderColor = Color(0xFFE0E0E0)\n                        )\n                    )\n                }\n            }\n            \n            // 滑块（仅对数值类型显示）\n            if (paramType in listOf(\"Int\", \"Float\", \"Double\") && (enumOptions.isNullOrEmpty() || paramType != \"Int\")) {\n                Slider(\n                    value = numericValue.coerceIn(minValue, maxValue),\n                    onValueChange = { newValue ->\n                        val snapped = if (step > 0f) {\n                            (newValue / step).roundToInt() * step\n                        } else {\n                            newValue\n                        }\n                        val next = when (paramType) {\n                            \"Int\" -> snapped.toInt().toString()\n                            \"Float\" -> String.format(Locale.US, \"%.${decimals}f\", snapped)\n                            \"Double\" -> String.format(Locale.US, \"%.${decimals}f\", snapped)\n                            else -> snapped.toString()\n                        }\n                        draftText = next\n                        lastValidText = next\n                        paramMap[Pair(paramKey, paramType)] = next\n                        onDirtyChange(true)\n                        isDragging = true\n                        pendingSamplePreview = true\n                    },\n                    onValueChangeFinished = {\n                        // 松手后：立即算一次，确保最终值立刻出预览\n                        isDragging = false\n                        pendingSamplePreview = false\n                        // 拖动即提交：松手时提交到编辑器（并生成一次历史节点）\n                        commitNow()\n                    },\n                    valueRange = minValue..maxValue,\n                    colors = SliderDefaults.colors(\n                        thumbColor = Color(0xFF007AFF),\n                        activeTrackColor = Color(0xFF007AFF)\n                    )\n                )\n            }\n\n            // 失焦时如果输入非法，回退到上一次有效值（符合 Spec）\n            LaunchedEffect(hasFocus) {\n                if (!hasFocus) {\n                    val ok = when (paramType) {\n                        \"Int\" -> draftText.safelyConvertToInt() != null\n                        \"Float\" -> draftText.toFloatOrNull() != null\n                        \"Double\" -> draftText.toDoubleOrNull() != null\n                        else -> true\n                    }\n                    if (!ok) {\n                        draftText = lastValidText\n                    } else if (draftText != lastValidText) {\n                        // 理论上不会发生（lastValidText 会同步），兜底：失焦提交一次\n                        commitNow()\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun EnumParamDropdown(\n    valueText: String,\n    options: List<FilterEnumOption>,\n    onSelect: (Int) -> Unit,\n    i18nState: I18nState\n) {\n    var expanded by remember { mutableStateOf(false) }\n    val currentInt = valueText.safelyConvertToInt()\n    val currentLabel = currentInt?.let { v ->\n        options.firstOrNull { it.value == v }?.let { opt -> i18nState.getString(opt.labelKey) }\n    } ?: valueText\n\n    Box(modifier = Modifier.width(140.dp)) {\n        OutlinedTextField(\n            value = if (currentInt != null) \"$currentLabel ($currentInt)\" else currentLabel,\n            onValueChange = {},\n            readOnly = true,\n            modifier = Modifier.fillMaxWidth(),\n            trailingIcon = {\n                IconButton(onClick = { expanded = true }) {\n                    Icon(Icons.Default.ArrowDropDown, contentDescription = null)\n                }\n            },\n            colors = TextFieldDefaults.outlinedTextFieldColors(\n                focusedBorderColor = Color(0xFF007AFF),\n                unfocusedBorderColor = Color(0xFFE0E0E0)\n            )\n        )\n\n        // 透明点击层：避免 TextField 在 Desktop 上吞掉点击事件导致无法展开\n        Box(\n            modifier = Modifier\n                .matchParentSize()\n                .clickable { expanded = true }\n        )\n\n        DropdownMenu(\n            expanded = expanded,\n            onDismissRequest = { expanded = false }\n        ) {\n            options.forEach { opt ->\n                DropdownMenuItem(onClick = {\n                    expanded = false\n                    onSelect(opt.value)\n                }) {\n                    Text(\"${i18nState.getString(opt.labelKey)} (${opt.value})\")\n                }\n            }\n        }\n    }\n}\n\n/**\n * Notes 区域\n */\n@Composable\nprivate fun FilterNotesSection(\n    filterName: String,\n    i18nState: I18nState\n) {\n    val remark = getFilterRemark(filterName)\n    \n    if (!remark.isNullOrEmpty()) {\n        Card(\n            modifier = Modifier.fillMaxWidth(),\n            elevation = 1.dp,\n            shape = RoundedCornerShape(8.dp),\n            backgroundColor = Color(0xFFEEEEEE)\n        ) {\n            Column(\n                modifier = Modifier.padding(16.dp),\n                verticalArrangement = Arrangement.spacedBy(8.dp)\n            ) {\n                Text(\n                    text = i18nState.getString(\"notes\"),\n                    fontSize = 14.sp,\n                    fontWeight = FontWeight.Bold,\n                    color = Color(0xFF222222)\n                )\n                Text(\n                    text = remark,\n                    fontSize = 12.sp,\n                    color = Color(0xFF222222),\n                    lineHeight = 18.sp\n                )\n            }\n        }\n    }\n}\n\n/**\n * 底部操作按钮\n */\n@Composable\nprivate fun FilterActionButtons(\n    modifier: Modifier = Modifier,\n    state: ApplicationState,\n    filterBaseImage: BufferedImage?,\n    viewModel: FilterViewModel,\n    selectedIndex: Int,\n    previewImage: BufferedImage?,\n    onPreviewImageChange: (BufferedImage?) -> Unit,\n    isDirty: Boolean,\n    onDirtyChange: (Boolean) -> Unit,\n    paramVersion: Int,\n    onParamVersionChange: (Int) -> Unit,\n    appliedParamSnapshot: Map<Pair<String, String>, String>,\n    onAppliedParamSnapshotChange: (Map<Pair<String, String>, String>) -> Unit,\n    paramMap: MutableMap<Pair<String, String>, String>,\n    onClearFilter: () -> Unit,\n    onShowToast: (String) -> Unit,\n    i18nState: I18nState\n) {\n    val hasImage = state.currentImage != null\n    val filterName = remember(selectedIndex) { filterNames[selectedIndex] }\n    val defaultMap = remember(selectedIndex) { buildDefaultParamMap(filterName) }\n    val isAtDefault = remember(defaultMap, paramMap) { paramMap == defaultMap }\n    val canCancel = (isDirty || previewImage != null)\n    val canReset = hasImage && !isAtDefault\n    val canClear = hasImage\n\n    Row(\n        modifier = modifier,\n        horizontalArrangement = Arrangement.SpaceBetween,\n        verticalAlignment = Alignment.CenterVertically\n    ) {\n        // 左侧：Reset + 清除滤镜\n        Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {\n            OutlinedButton(\n                onClick = {\n                    paramMap.clear()\n                    paramMap.putAll(defaultMap)\n                    // 拖动即提交：Reset 也直接提交\n                    val base = filterBaseImage ?: state.currentImage ?: state.rawImage\n                    if (base != null) {\n                        viewModel.applyFilter(\n                            state = state,\n                            index = selectedIndex,\n                            paramMap = HashMap(paramMap),\n                            sourceImage = base,\n                            pushHistory = true\n                        )\n                    }\n                    onDirtyChange(false)\n                    onPreviewImageChange(null)\n                    onAppliedParamSnapshotChange(HashMap(paramMap))\n                    onParamVersionChange(paramVersion + 1)\n                },\n                enabled = canReset,\n                modifier = Modifier.height(40.dp),\n                colors = ButtonDefaults.outlinedButtonColors(\n                    contentColor = Color(0xFF222222)\n                )\n            ) {\n                Text(\n                    text = i18nState.getString(\"reset_filter\"),\n                    fontSize = 14.sp,\n                    fontWeight = FontWeight.Medium\n                )\n            }\n\n            OutlinedButton(\n                onClick = onClearFilter,\n                enabled = canClear,\n                modifier = Modifier.height(40.dp),\n                colors = ButtonDefaults.outlinedButtonColors(\n                    contentColor = Color(0xFF222222)\n                )\n            ) {\n                Text(\n                    text = i18nState.getString(\"clear_filter\"),\n                    fontSize = 14.sp,\n                    fontWeight = FontWeight.Medium\n                )\n            }\n        }\n\n        // 右侧：Cancel + Apply\n        Row(\n            horizontalArrangement = Arrangement.spacedBy(8.dp),\n            verticalAlignment = Alignment.CenterVertically\n        ) {\n            OutlinedButton(\n                onClick = {\n                    // Cancel：回到上次提交后的参数（仅用于取消未松手/未提交的预览状态）\n                    paramMap.clear()\n                    paramMap.putAll(appliedParamSnapshot)\n                    onParamVersionChange(paramVersion + 1)\n                    onPreviewImageChange(null)\n                    onDirtyChange(false)\n                },\n                enabled = canCancel,\n                modifier = Modifier.height(40.dp),\n                colors = ButtonDefaults.outlinedButtonColors(\n                    contentColor = Color(0xFF222222)\n                )\n            ) {\n                Text(\n                    text = i18nState.getString(\"cancel\"),\n                    fontSize = 14.sp,\n                    fontWeight = FontWeight.Medium\n                )\n            }\n        }\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/filter/widget/FilterListPanel.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.filter.widget\n\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.*\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Search\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.painter.Painter\nimport androidx.compose.ui.graphics.toPainter\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport cn.netdiscovery.monica.state.ApplicationState\nimport cn.netdiscovery.monica.ui.i18n.I18nState\nimport filterMaps\nimport filterNames\n\n/**\n * 左侧滤镜列表面板\n */\n@Composable\nfun FilterListPanel(\n    modifier: Modifier = Modifier,\n    searchQuery: String,\n    onSearchQueryChange: (String) -> Unit,\n    selectedIndex: Int,\n    onFilterSelected: (Int) -> Unit,\n    state: ApplicationState,\n    i18nState: I18nState\n) {\n    val currentImagePainter = remember(state.currentImage) { state.currentImage?.toPainter() }\n\n    Surface(\n        modifier = modifier.fillMaxHeight(),\n        color = Color.White,\n        elevation = 1.dp\n    ) {\n        Column(\n            modifier = Modifier.fillMaxSize()\n        ) {\n            // 搜索栏\n            OutlinedTextField(\n                value = searchQuery,\n                onValueChange = onSearchQueryChange,\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .padding(12.dp),\n                placeholder = {\n                    Text(\n                        text = i18nState.getString(\"search\"),\n                        fontSize = 14.sp\n                    )\n                },\n                leadingIcon = {\n                    Icon(\n                        imageVector = Icons.Default.Search,\n                        contentDescription = null,\n                        tint = Color(0xFF666666)\n                    )\n                },\n                singleLine = true,\n                colors = TextFieldDefaults.outlinedTextFieldColors(\n                    focusedBorderColor = Color(0xFF007AFF),\n                    unfocusedBorderColor = Color(0xFFE0E0E0)\n                ),\n                shape = RoundedCornerShape(8.dp)\n            )\n            \n            // 滤镜列表\n            val filteredFilters = remember(searchQuery, filterNames) {\n                if (searchQuery.isBlank()) {\n                    filterNames.indices.toList()\n                } else {\n                    filterNames.indices.filter { index ->\n                        filterNames[index].contains(searchQuery, ignoreCase = true) ||\n                        (filterMaps[filterNames[index]]?.contains(searchQuery, ignoreCase = true) == true)\n                    }\n                }\n            }\n            \n            LazyColumn(\n                modifier = Modifier.fillMaxSize(),\n                contentPadding = PaddingValues(horizontal = 12.dp, vertical = 8.dp),\n                verticalArrangement = Arrangement.spacedBy(8.dp)\n            ) {\n                if (filteredFilters.isEmpty()) {\n                    item {\n                        Text(\n                            text = i18nState.getString(\"no_filters_found\"),\n                            fontSize = 12.sp,\n                            color = Color(0xFF999999),\n                            modifier = Modifier.padding(vertical = 12.dp)\n                        )\n                    }\n                    return@LazyColumn\n                }\n\n                itemsIndexed(items = filteredFilters, key = { _, filterIndex -> filterIndex }) { _, filterIndex ->\n                    val isSelected = filterIndex == selectedIndex\n                    val filterName = filterNames[filterIndex]\n                    \n                    FilterListItem(\n                        filterName = filterName,\n                        isSelected = isSelected,\n                        onClick = { onFilterSelected(filterIndex) },\n                        state = state,\n                        imagePainter = currentImagePainter,\n                        noImageText = i18nState.getString(\"no_image\"),\n                        modifier = Modifier.fillMaxWidth()\n                    )\n                }\n            }\n        }\n    }\n}\n\n/**\n * 单个滤镜列表项\n */\n@Composable\nprivate fun FilterListItem(\n    filterName: String,\n    isSelected: Boolean,\n    onClick: () -> Unit,\n    state: ApplicationState,\n    imagePainter: Painter?,\n    noImageText: String,\n    modifier: Modifier = Modifier\n) {\n    Card(\n        modifier = modifier\n            .height(80.dp)\n            .clickable(onClick = onClick),\n        elevation = if (isSelected) 4.dp else 1.dp,\n        shape = RoundedCornerShape(8.dp),\n        backgroundColor = if (isSelected) {\n            Color(0xFFE3F2FD) // 淡蓝背景\n        } else {\n            Color.White\n        }\n    ) {\n        Row(\n            modifier = Modifier\n                .fillMaxSize()\n                .padding(4.dp),\n            verticalAlignment = Alignment.CenterVertically,\n            horizontalArrangement = Arrangement.spacedBy(12.dp)\n        ) {\n            // 左侧选中指示条\n            if (isSelected) {\n                Box(\n                    modifier = Modifier\n                        .width(4.dp)\n                        .fillMaxHeight()\n                        .background(\n                            color = Color(0xFF007AFF),\n                            shape = RoundedCornerShape(2.dp)\n                        )\n                )\n            } else {\n                Spacer(modifier = Modifier.width(4.dp))\n            }\n            \n            // 缩略图预览（使用当前图像的小缩略图）\n            Box(\n                modifier = Modifier\n                    .size(60.dp)\n                    .background(\n                        color = Color(0xFFF5F5F5),\n                        shape = RoundedCornerShape(4.dp)\n                    ),\n                contentAlignment = Alignment.Center\n            ) {\n                if (imagePainter != null) {\n                    // 这里可以显示应用滤镜后的缩略图，简化版先显示原图\n                    Image(\n                        painter = imagePainter,\n                        contentDescription = null,\n                        contentScale = ContentScale.Crop,\n                        modifier = Modifier.fillMaxSize()\n                    )\n                } else {\n                    Text(\n                        text = noImageText,\n                        fontSize = 12.sp,\n                        color = Color(0xFF999999)\n                    )\n                }\n            }\n            \n            // 滤镜名称\n            Column(\n                modifier = Modifier.weight(1f),\n                verticalArrangement = Arrangement.Center\n            ) {\n                Text(\n                    text = filterName,\n                    fontSize = 14.sp,\n                    fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,\n                    color = if (isSelected) {\n                        Color(0xFF007AFF)\n                    } else {\n                        Color(0xFF222222)\n                    }\n                )\n                // 滤镜描述（如果有）\n                filterMaps[filterName]?.split(\"-\")?.firstOrNull()?.let { desc ->\n                    Text(\n                        text = desc,\n                        fontSize = 12.sp,\n                        color = Color(0xFF666666),\n                        maxLines = 1\n                    )\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/filter/widget/FilterParamDefaults.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.filter.widget\n\nimport cn.netdiscovery.monica.rxcache.Param\nimport cn.netdiscovery.monica.rxcache.getFilterParam\nimport cn.netdiscovery.monica.utils.extensions.safelyConvertToInt\nimport java.util.Locale\nimport kotlin.math.max\n\n/**\n * 生成某个滤镜的“默认参数”Map，用于 Reset / 初始化 / 判断是否处于默认状态。\n *\n * 注意：\n * - Float/Double 会按 [FilterParamMetaRegistry] 的 decimals 格式化，避免 UI 显示不一致。\n */\nfun buildDefaultParamMap(filterName: String): Map<Pair<String, String>, String> {\n    val params: List<Param> = getFilterParam(filterName).orEmpty()\n    val result = LinkedHashMap<Pair<String, String>, String>(params.size)\n\n    params.forEach { param ->\n        val meta = FilterParamMetaRegistry.resolve(filterName = filterName, param = param)\n        val key = Pair(param.key, param.type)\n        val defaultValue = when (param.type) {\n            \"Int\" -> {\n                val raw = param.value.toString().safelyConvertToInt() ?: 0\n                // 兜底：像 BlockFilter 的 blockSize 这种会作为 step 的参数，必须 >= meta.min\n                max(meta.min.toInt(), raw).toString()\n            }\n            \"Float\" -> {\n                val v = param.value.toString().toDoubleOrNull() ?: 0.0\n                String.format(Locale.US, \"%.${meta.decimals}f\", v)\n            }\n            \"Double\" -> {\n                val v = param.value.toString().toDoubleOrNull() ?: 0.0\n                String.format(Locale.US, \"%.${meta.decimals}f\", v)\n            }\n            else -> param.value.toString()\n        }\n        result[key] = defaultValue\n    }\n    return result\n}\n\n\n"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/filter/widget/FilterParamMeta.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.filter.widget\n\nimport cn.netdiscovery.monica.rxcache.Param\nimport kotlin.math.max\n\n/**\n * 滤镜参数的 UI 元信息（范围/步长/格式），用于让 Slider 的体验可配置且一致。\n */\ndata class FilterEnumOption(\n    val value: Int,\n    val labelKey: String\n)\n\ndata class FilterParamMeta(\n    val min: Float,\n    val max: Float,\n    val step: Float,\n    val decimals: Int,\n    val enumOptions: List<FilterEnumOption>? = null\n)\n\nobject FilterParamMetaRegistry {\n\n    private const val DEFAULT_INT_MIN = 0f\n    private const val DEFAULT_INT_MAX = 100f\n\n    private const val DEFAULT_FLOAT_MIN = 0f\n    private const val DEFAULT_FLOAT_MAX = 10f\n\n    /**\n     * 参数范围/步长/格式的覆盖表（可维护的“配置”）。\n     *\n     * 优先级：\n     * 1) filterName + paramKey 精确覆盖（最精细，避免误伤）\n     * 2) paramKey 通用覆盖（兜底）\n     * 3) 按类型默认\n     *\n     * 说明：目前覆盖表写在 Kotlin 里，后续如需完全配置化，可迁移到 json 并在此加载。\n     */\n    private val filterKeyOverrides: Map<String, Map<String, FilterParamMeta>> = mapOf(\n        \"ColorFilter\" to mapOf(\n            \"style\" to FilterParamMeta(\n                min = 0f,\n                max = 11f,\n                step = 1f,\n                decimals = 0,\n                enumOptions = listOf(\n                    FilterEnumOption(0, \"color_filter_style_0\"),\n                    FilterEnumOption(1, \"color_filter_style_1\"),\n                    FilterEnumOption(2, \"color_filter_style_2\"),\n                    FilterEnumOption(3, \"color_filter_style_3\"),\n                    FilterEnumOption(4, \"color_filter_style_4\"),\n                    FilterEnumOption(5, \"color_filter_style_5\"),\n                    FilterEnumOption(6, \"color_filter_style_6\"),\n                    FilterEnumOption(7, \"color_filter_style_7\"),\n                    FilterEnumOption(8, \"color_filter_style_8\"),\n                    FilterEnumOption(9, \"color_filter_style_9\"),\n                    FilterEnumOption(10, \"color_filter_style_10\"),\n                    FilterEnumOption(11, \"color_filter_style_11\")\n                )\n            )\n        ),\n        \"NatureFilter\" to mapOf(\n            \"style\" to FilterParamMeta(\n                min = 1f,\n                max = 8f,\n                step = 1f,\n                decimals = 0,\n                enumOptions = listOf(\n                    FilterEnumOption(1, \"nature_filter_style_1\"),\n                    FilterEnumOption(2, \"nature_filter_style_2\"),\n                    FilterEnumOption(3, \"nature_filter_style_3\"),\n                    FilterEnumOption(4, \"nature_filter_style_4\"),\n                    FilterEnumOption(5, \"nature_filter_style_5\"),\n                    FilterEnumOption(6, \"nature_filter_style_6\"),\n                    FilterEnumOption(7, \"nature_filter_style_7\"),\n                    FilterEnumOption(8, \"nature_filter_style_8\")\n                )\n            )\n        ),\n        \"BlockFilter\" to mapOf(\n            // BlockFilter：blockSize 会被用作 Kotlin range 的 step，必须 > 0\n            \"blocksize\" to FilterParamMeta(min = 1f, max = 128f, step = 1f, decimals = 0)\n        ),\n        \"CropFilter\" to mapOf(\n            // CropFilter：x, y 为起始坐标，w, h 为裁剪区域宽高\n            // 设置较大的最大值以支持高分辨率图片裁剪（最大支持 8192 像素）\n            \"x\" to FilterParamMeta(min = 0f, max = 8192f, step = 1f, decimals = 0),\n            \"y\" to FilterParamMeta(min = 0f, max = 8192f, step = 1f, decimals = 0),\n            \"w\" to FilterParamMeta(min = 1f, max = 8192f, step = 1f, decimals = 0),\n            \"h\" to FilterParamMeta(min = 1f, max = 8192f, step = 1f, decimals = 0)\n        )\n    )\n\n    /**\n     * 基于参数名/类型给出一个“默认但可维护”的范围配置。\n     * 后续如需更精细（按 filterName+paramKey），可以在这里加覆盖表。\n     */\n    fun resolve(filterName: String, param: Param): FilterParamMeta {\n        val paramKey = param.key.lowercase()\n\n        // 1) filterName + paramKey 精确覆盖\n        filterKeyOverrides[filterName]?.get(paramKey)?.let { return it }\n\n        // 类型默认\n        return when (param.type) {\n            \"Int\" -> FilterParamMeta(\n                min = DEFAULT_INT_MIN,\n                max = max(DEFAULT_INT_MAX, DEFAULT_INT_MIN + 1f),\n                step = 1f,\n                decimals = 0\n            )\n            \"Float\", \"Double\" -> FilterParamMeta(\n                min = DEFAULT_FLOAT_MIN,\n                max = max(DEFAULT_FLOAT_MAX, DEFAULT_FLOAT_MIN + 0.01f),\n                step = 0.01f,\n                decimals = 2\n            )\n            else -> FilterParamMeta(\n                min = DEFAULT_INT_MIN,\n                max = DEFAULT_INT_MAX,\n                step = 1f,\n                decimals = 0\n            )\n        }\n    }\n}\n\n\n\n"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/filter/widget/FilterPreviewArea.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.filter.widget\n\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.*\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Add\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.toPainter\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.graphics.graphicsLayer\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport cn.netdiscovery.monica.state.ApplicationState\nimport cn.netdiscovery.monica.ui.i18n.I18nState\nimport java.awt.image.BufferedImage\n\n/**\n * 中间图像预览区域\n */\n@Composable\nfun FilterPreviewArea(\n    modifier: Modifier = Modifier,\n    state: ApplicationState,\n    previewImage: BufferedImage?,\n    zoomLevel: Float,\n    onZoomChange: (Float) -> Unit,\n    onImageClick: () -> Unit,\n    i18nState: I18nState\n) {\n    Surface(\n        modifier = modifier.fillMaxHeight(),\n        color = Color(0xFFF5F5F5),\n        elevation = 1.dp\n    ) {\n        Box(\n            modifier = Modifier.fillMaxSize(),\n            contentAlignment = Alignment.Center\n        ) {\n            // 显示预览图像或当前图像\n            val displayImage = previewImage ?: state.currentImage\n            \n            if (displayImage != null) {\n                // 图像预览\n                Card(\n                    modifier = Modifier\n                        .fillMaxSize()\n                        .padding(16.dp),\n                    shape = RoundedCornerShape(8.dp),\n                    elevation = 2.dp,\n                    backgroundColor = Color.White\n                ) {\n                    Box(\n                        modifier = Modifier.fillMaxSize(),\n                        contentAlignment = Alignment.Center\n                    ) {\n                        Image(\n                            painter = displayImage.toPainter(),\n                            contentDescription = null,\n                            contentScale = ContentScale.Fit,\n                            modifier = Modifier\n                                .fillMaxSize()\n                                .graphicsLayer(\n                                    scaleX = zoomLevel,\n                                    scaleY = zoomLevel\n                                )\n                        )\n                    }\n                }\n            } else {\n                // 空状态提示\n                Card(\n                    modifier = Modifier\n                        .fillMaxSize()\n                        .padding(16.dp)\n                        .clickable(onClick = onImageClick),\n                    shape = RoundedCornerShape(8.dp),\n                    elevation = 2.dp,\n                    backgroundColor = Color.White\n                ) {\n                    Box(\n                        modifier = Modifier.fillMaxSize(),\n                        contentAlignment = Alignment.Center\n                    ) {\n                        Column(\n                            horizontalAlignment = Alignment.CenterHorizontally,\n                            verticalArrangement = Arrangement.spacedBy(8.dp)\n                        ) {\n                            Text(\n                                text = i18nState.getString(\"click_to_select_image\"),\n                                fontSize = 18.sp,\n                                color = Color(0xFF666666),\n                                textAlign = TextAlign.Center\n                            )\n                        }\n                    }\n                }\n            }\n            \n            // 底部缩放控制栏\n            Surface(\n                modifier = Modifier\n                    .align(Alignment.BottomCenter)\n                    .padding(bottom = 16.dp),\n                shape = RoundedCornerShape(20.dp),\n                color = Color.White.copy(alpha = 0.9f),\n                elevation = 4.dp\n            ) {\n                Row(\n                    modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp),\n                    verticalAlignment = Alignment.CenterVertically,\n                    horizontalArrangement = Arrangement.spacedBy(8.dp)\n                ) {\n                    // 缩小按钮\n                    TextButton(\n                        onClick = {\n                            onZoomChange((zoomLevel - 0.1f).coerceAtLeast(0.1f))\n                        },\n                        modifier = Modifier.height(32.dp)\n                    ) {\n                        Text(\n                            text = \"−\",\n                            fontSize = 18.sp,\n                            color = Color(0xFF222222),\n                            fontWeight = FontWeight.Bold\n                        )\n                    }\n                    \n                    // 缩放百分比显示\n                    Text(\n                        text = \"${(zoomLevel * 100).toInt()}%\",\n                        fontSize = 14.sp,\n                        fontWeight = FontWeight.Medium,\n                        color = Color(0xFF222222),\n                        modifier = Modifier.width(50.dp),\n                        textAlign = TextAlign.Center\n                    )\n                    \n                    // 放大按钮\n                    IconButton(\n                        onClick = {\n                            onZoomChange((zoomLevel + 0.1f).coerceAtMost(5.0f))\n                        },\n                        modifier = Modifier.size(32.dp)\n                    ) {\n                        Icon(\n                            imageVector = Icons.Default.Add,\n                            contentDescription = \"Zoom In\",\n                            tint = Color(0xFF222222)\n                        )\n                    }\n                    \n                    // 分隔线\n                    Divider(\n                        modifier = Modifier\n                            .height(24.dp)\n                            .width(1.dp),\n                        color = Color(0xFFE0E0E0)\n                    )\n                    \n                    // 适应屏幕按钮（使用文本代替图标）\n                    TextButton(\n                        onClick = {\n                            onZoomChange(1.0f)\n                        },\n                        modifier = Modifier.height(32.dp)\n                    ) {\n                        Text(\n                            text = i18nState.getString(\"fit\"),\n                            fontSize = 12.sp,\n                            color = Color(0xFF222222)\n                        )\n                    }\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/filter/widget/FilterTopAppBar.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.filter.widget\n\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.material.*\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport cn.netdiscovery.monica.ui.i18n.I18nState\n\n/**\n * 滤镜模块顶部应用栏\n */\n@Composable\nfun FilterTopAppBar(\n    onSave: () -> Unit,\n    onExport: () -> Unit,\n    i18nState: I18nState\n) {\n    Surface(\n        modifier = Modifier\n            .fillMaxWidth()\n            .height(56.dp),\n        elevation = 2.dp,\n        color = Color.White\n    ) {\n        Row(\n            modifier = Modifier\n                .fillMaxWidth()\n                .padding(horizontal = 16.dp),\n            verticalAlignment = Alignment.CenterVertically,\n            horizontalArrangement = Arrangement.SpaceBetween\n        ) {\n            // 左侧标题和菜单\n            Row(\n                verticalAlignment = Alignment.CenterVertically,\n                horizontalArrangement = Arrangement.spacedBy(16.dp)\n            ) {\n                Text(\n                    text = i18nState.getString(\"image_editor_filter_module\"),\n                    fontSize = 18.sp,\n                    fontWeight = FontWeight.Bold,\n                    color = Color(0xFF222222)\n                )\n            }\n            \n            // 右侧按钮\n            Row(\n                horizontalArrangement = Arrangement.spacedBy(8.dp),\n                verticalAlignment = Alignment.CenterVertically\n            ) {\n                // Save 按钮（次要按钮）\n                Button(\n                    onClick = onSave,\n                    colors = ButtonDefaults.buttonColors(\n                        backgroundColor = Color(0xFFE0E0E0),\n                        contentColor = Color(0xFF222222)\n                    ),\n                    modifier = Modifier.height(36.dp)\n                ) {\n                    Text(\n                        text = i18nState.getString(\"save\"),\n                        fontSize = 14.sp,\n                        fontWeight = FontWeight.Medium\n                    )\n                }\n                \n                // Export 按钮（主要按钮）\n                Button(\n                    onClick = onExport,\n                    colors = ButtonDefaults.buttonColors(\n                        backgroundColor = Color(0xFF007AFF),\n                        contentColor = Color.White\n                    ),\n                    modifier = Modifier.height(36.dp)\n                ) {\n                    Text(\n                        text = i18nState.getString(\"export\"),\n                        fontSize = 14.sp,\n                        fontWeight = FontWeight.Medium\n                    )\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/generategif/GenerateGifView.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.generategif\n\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.lazy.grid.GridCells\nimport androidx.compose.foundation.lazy.grid.LazyVerticalGrid\nimport androidx.compose.foundation.lazy.grid.itemsIndexed\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.*\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.painter.BitmapPainter\nimport androidx.compose.ui.graphics.toComposeImageBitmap\nimport androidx.compose.ui.unit.dp\nimport cn.netdiscovery.monica.state.ApplicationState\nimport cn.netdiscovery.monica.ui.i18n.rememberI18nState\nimport cn.netdiscovery.monica.ui.widget.*\nimport cn.netdiscovery.monica.utils.chooseImage\nimport cn.netdiscovery.monica.utils.getBufferedImage\nimport cn.netdiscovery.monica.utils.getValidateField\nimport org.koin.compose.koinInject\nimport java.io.File\nimport loadingDisplay\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.generategif.GenerateGifView\n * @author: Tony Shen\n * @date:  2025/2/23 16:16\n * @version: V1.0 <描述当前版本功能>\n */\nprivate var showVerifyToast by mutableStateOf(false)\nprivate var verifyToastMessage by mutableStateOf(\"\")\nprivate val height = 600.dp // 上传图片的区域\n\n@OptIn(ExperimentalMaterialApi::class)\n@Composable\nfun generateGif(state: ApplicationState) {\n    val i18nState = rememberI18nState()\n    val viewModel: GenerateGifViewModel = koinInject()\n\n    var selectedImages by remember { mutableStateOf<List<File>>(emptyList()) }\n\n    var widthText by remember { mutableStateOf(\"400\") }\n    var heightText by remember { mutableStateOf(\"400\") }\n    var frameDelayText by remember { mutableStateOf(\"500\") }\n    var loopEnabled by remember { mutableStateOf(false) }\n\n    fun clear() {\n        widthText = \"400\"\n        heightText = \"400\"\n        frameDelayText = \"500\"\n        loopEnabled = false\n        selectedImages = emptyList()\n    }\n\n    @Composable\n    fun addImageCard(state:ApplicationState) {\n        Card(onClick = {\n            chooseImage(state) {imageFile ->\n                selectedImages += imageFile\n            }},\n            modifier = Modifier.padding(10.dp).width(300.dp).height(150.dp), shape = RoundedCornerShape(8.dp))  {\n            Row(horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically) {\n                Text(i18nState.getString(\"add_image_first\"))\n            }\n        }\n    }\n\n    Box(\n        Modifier.fillMaxSize(),\n        contentAlignment = Alignment.Center\n    ) {\n        Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {\n\n            if (selectedImages.isNotEmpty()) {\n                subTitle(modifier = Modifier.padding(start = 20.dp), text = i18nState.getString(\"select_images\"), color = Color.Black)\n\n                Box(modifier = Modifier.height(height).fillMaxWidth()) {\n                    LazyVerticalGrid(\n                        columns = GridCells.Fixed(5),\n                        modifier = Modifier.fillMaxWidth()\n                    ) {\n                        val emptyFile = File(\"\")\n                        itemsIndexed(selectedImages + emptyFile) { index, imageFile ->\n\n                            if (index < selectedImages.size) {\n                                Card(modifier = Modifier.padding(10.dp), shape = RoundedCornerShape(8.dp)) {\n\n                                    Column(modifier = Modifier.padding(4.dp), horizontalAlignment = Alignment.CenterHorizontally) {\n                                        val bitmap = remember(imageFile) { getBufferedImage(imageFile).toComposeImageBitmap() }\n\n                                        Image(painter = BitmapPainter(bitmap), contentDescription = imageFile.name, modifier = Modifier.size(100.dp))\n\n                                        Row(horizontalArrangement = Arrangement.SpaceEvenly) {\n                                            Button(onClick = {\n                                                selectedImages = selectedImages.toMutableList().apply { removeAt(index) }\n                                            }) {\n                                                Text(\"Delete\")\n                                            }\n\n                                            if (index > 0) {\n                                                Button(onClick = {\n                                                    selectedImages = selectedImages.toMutableList().apply {\n                                                        add(index - 1, removeAt(index))\n                                                    }\n                                                }) {\n                                                    Text(\"Up\")\n                                                }\n                                            }\n\n                                            if (index < selectedImages.size - 1) {\n                                                Button(onClick = {\n                                                    selectedImages = selectedImages.toMutableList().apply {\n                                                        add(index + 1, removeAt(index))\n                                                    }\n                                                }) {\n                                                    Text(\"Down\")\n                                                }\n                                            }\n                                        }\n                                    }\n                                }\n                            } else {\n                                addImageCard(state)\n                            }\n                        }\n                    }\n                }\n            } else {\n                Spacer(modifier = Modifier.height(16.dp))\n\n                Column(modifier = Modifier.height(height).fillMaxWidth()) {\n                    addImageCard(state)\n                }\n            }\n\n            Spacer(modifier = Modifier.height(16.dp))\n\n            // gif 生成策略\n            subTitleWithDivider(text = i18nState.getString(\"gif_generation_strategy\"), color = Color.Black)\n\n            Row {\n                basicTextFieldWithTitle(titleText = i18nState.getString(\"gif_width\"), widthText, Modifier.padding(end = 20.dp)) { str ->\n                    widthText = str\n                }\n\n                basicTextFieldWithTitle(titleText = i18nState.getString(\"gif_height\"), heightText, Modifier.padding(end = 20.dp)) { str ->\n                    heightText = str\n                }\n            }\n\n            Row(modifier = Modifier.padding(top = 20.dp)) {\n                // 每一帧间隔 (ms)\n                basicTextFieldWithTitle(titleText = i18nState.getString(\"frame_interval\"), frameDelayText) { str ->\n                    frameDelayText = str\n                }\n            }\n\n            Row(modifier = Modifier.padding(top = 16.dp), verticalAlignment = Alignment.CenterVertically) {\n                Text(i18nState.getString(\"loop_playback\"))\n                Checkbox(checked = loopEnabled, onCheckedChange = { loopEnabled = it })\n            }\n\n            Spacer(modifier = Modifier.height(40.dp))\n\n            Row {\n                confirmButton(\n                    enabled = true,\n                    text = \"返回首页\",\n                    onClick = {\n                        clear()\n                        state.closePreviewWindow()\n                    })\n\n                confirmButton(\n                    enabled = true,\n                    text = \"清空图片\",\n                    modifier = Modifier.padding(start = 20.dp),\n                    onClick = {\n                        clear()\n                    })\n\n                confirmButton(\n                    enabled = selectedImages.isNotEmpty(),\n                    text = \"生成 gif\",\n                    modifier = Modifier.padding(start = 20.dp),\n                    onClick = {\n                        val width = getValidateField(block = { widthText.toInt() } , failed = { showGenerateGifVerifyToast(\"width 需要 int 类型\") }) ?: return@confirmButton\n                        val height = getValidateField(block = { heightText.toInt() } , failed = { showGenerateGifVerifyToast(\"height 需要 int 类型\") }) ?: return@confirmButton\n                        val frameDelay = getValidateField(block = { frameDelayText.toInt() } , failed = { showGenerateGifVerifyToast(\"frameDelay 需要 int 类型\") }) ?: return@confirmButton\n\n                        viewModel.generateGif(state, selectedImages, width, height, frameDelay, loopEnabled) {\n                            showGenerateGifVerifyToast(\"gif 已生成\")\n                            clear()\n                        }\n                    })\n            }\n        }\n\n        if (loadingDisplay) {\n            showLoading()\n        }\n\n        if (showVerifyToast) {\n            centerToast(message = verifyToastMessage) {\n                showVerifyToast = false\n            }\n        }\n    }\n}\n\nprivate fun showGenerateGifVerifyToast(message: String) {\n    verifyToastMessage = message\n    showVerifyToast = true\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/generategif/GenerateGifViewModel.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.generategif\n\nimport cn.netdiscovery.monica.state.ApplicationState\nimport cn.netdiscovery.monica.utils.Action\nimport cn.netdiscovery.monica.utils.currentTime\nimport cn.netdiscovery.monica.utils.extensions.launchWithLoading\nimport com.madgag.gif.fmsware.AnimatedGifEncoder\nimport org.slf4j.Logger\nimport org.slf4j.LoggerFactory\nimport java.io.File\nimport java.io.FileOutputStream\nimport javax.imageio.ImageIO\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.generategif.GenerateGifViewModel\n * @author: Tony Shen\n * @date: 2025/3/4 14:06\n * @version: V1.0 <描述当前版本功能>\n */\nclass GenerateGifViewModel {\n\n    private val logger: Logger = LoggerFactory.getLogger(object : Any() {}.javaClass.enclosingClass)\n\n    fun generateGif(state: ApplicationState,images: List<File>, width: Int, height: Int, frameDelay: Int, loopEnabled: Boolean, block: Action) {\n        logger.info(\"start to generate gif\")\n\n        state.scope.launchWithLoading {\n            val gifEncoder = AnimatedGifEncoder()\n            gifEncoder.setSize(width, height)\n            gifEncoder.start(FileOutputStream(\"output_${currentTime()}.gif\"))\n\n            gifEncoder.setDelay(frameDelay)\n            gifEncoder.setRepeat(if (loopEnabled) 0 else 1) // Set loop option\n\n            images.forEach { imageFile ->\n                val image = ImageIO.read(imageFile)\n                gifEncoder.addFrame(image)\n            }\n\n            gifEncoder.finish()\n            logger.info(\"gif generated successfully!\")\n\n            block()\n        }\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/shapedrawing/CoordinateSystem.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.shapedrawing\n\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.unit.Density\nimport cn.netdiscovery.monica.utils.logger\nimport org.slf4j.Logger\nimport kotlin.math.pow\nimport kotlin.math.sqrt\n\n/**\n * 坐标系统工具类\n * 统一处理坐标转换、边界检查和验证\n * \n * @author Tony Shen\n * @date 2025/9/1 16:09\n * @version V1.0\n */\nobject CoordinateSystem {\n    private val logger: Logger = logger<CoordinateSystem>()\n    \n    /**\n     * 坐标验证结果\n     */\n    data class ValidationResult(\n        val isValid: Boolean,\n        val message: String = \"\",\n        val correctedOffset: Offset? = null\n    )\n    \n    /**\n     * 验证坐标是否有效\n     */\n    fun validateOffset(offset: Offset, imageWidth: Int, imageHeight: Int): ValidationResult {\n        return when {\n            offset == Offset.Unspecified -> {\n                ValidationResult(false, \"坐标未指定\")\n            }\n            offset.x.isNaN() || offset.y.isNaN() -> {\n                ValidationResult(false, \"坐标包含NaN值\")\n            }\n            offset.x.isInfinite() || offset.y.isInfinite() -> {\n                ValidationResult(false, \"坐标包含无穷值\")\n            }\n            offset.x < 0 || offset.y < 0 -> {\n                ValidationResult(false, \"坐标超出图像边界（负值）\")\n            }\n            offset.x > imageWidth || offset.y > imageHeight -> {\n                ValidationResult(false, \"坐标超出图像边界\")\n            }\n            else -> {\n                ValidationResult(true, \"坐标有效\")\n            }\n        }\n    }\n    \n    /**\n     * 计算文本位置（考虑文本尺寸和边界）\n     * @param dragOffset 拖拽偏移量（像素，相对于画布/图像显示中心）\n     * @param imageWidth 图像显示宽度（像素）\n     * @param imageHeight 图像显示高度（像素）\n     * @param density 屏幕密度\n     * @param textFieldWidth 文本输入框宽度（dp）\n     * @param textFieldHeight 文本输入框高度（dp）\n     * @param fontSize 字体大小（用于文本居中对齐）\n     * @return 文本在Canvas中的位置（Canvas坐标，以Canvas左上角为原点，像素）\n     */\n    fun calculateTextPosition(\n        dragOffset: Offset,\n        imageWidth: Int,\n        imageHeight: Int,\n        density: Density,\n        textFieldWidth: Float = 250f,\n        textFieldHeight: Float = 130f,\n        fontSize: Float = 40f\n    ): Offset {\n        // dragOffset 是文本输入框中心相对于画布中心的偏移（像素）\n        // 画布显示尺寸等于图像显示尺寸（imageWidth x imageHeight）\n        // 画布中心在 (imageWidth/2, imageHeight/2)\n        val canvasCenterX = imageWidth / 2f\n        val canvasCenterY = imageHeight / 2f\n        \n        // 文本中心位置 = 画布中心 + 拖拽偏移\n        val textCenterX = canvasCenterX + dragOffset.x\n        val textCenterY = canvasCenterY + dragOffset.y\n        \n        // 确保文本中心不会超出图像边界（考虑文本可能的最大宽度和高度）\n        // 使用 fontSize 作为文本高度的近似值，文本宽度需要根据实际文本内容计算\n        // 这里使用一个保守的估计值\n        val estimatedTextHeight = fontSize * 1.2f // 字体高度的1.2倍作为安全边距\n        val textFieldWidthPx = textFieldWidth * density.density\n        val estimatedTextWidth = textFieldWidthPx * 0.8f // 使用输入框宽度的80%作为文本宽度的估计\n        \n        val clampedX = textCenterX.coerceIn(estimatedTextWidth / 2f, imageWidth - estimatedTextWidth / 2f)\n        val clampedY = textCenterY.coerceIn(estimatedTextHeight / 2f, imageHeight - estimatedTextHeight / 2f)\n        \n        val canvasPosition = Offset(clampedX, clampedY)\n        \n        logger.info(\"文本位置计算: 拖拽偏移=$dragOffset, Canvas中心=($canvasCenterX, $canvasCenterY), 文本中心=($textCenterX, $textCenterY), 修正位置=($clampedX, $clampedY), 最终Canvas位置=$canvasPosition\")\n        \n        return canvasPosition\n    }\n    \n    /**\n     * 检查点是否在图像范围内\n     */\n    fun isPointInImage(point: Offset, imageWidth: Int, imageHeight: Int): Boolean {\n        return point.x >= 0 && point.x <= imageWidth && \n               point.y >= 0 && point.y <= imageHeight\n    }\n    \n    /**\n     * 计算两点之间的距离\n     */\n    fun calculateDistance(point1: Offset, point2: Offset): Float {\n        return sqrt((point2.x - point1.x).pow(2) + (point2.y - point1.y).pow(2))\n    }\n    \n    /**\n     * 计算圆的半径\n     */\n    fun calculateCircleRadius(center: Offset, pointOnCircle: Offset): Float {\n        return calculateDistance(center, pointOnCircle)\n    }\n    \n    /**\n     * 验证形状的边界\n     */\n    fun validateShapeBoundary(\n        points: List<Offset>, \n        imageWidth: Int, \n        imageHeight: Int\n    ): ValidationResult {\n        if (points.isEmpty()) {\n            return ValidationResult(false, \"形状没有顶点\")\n        }\n        \n        val invalidPoints = points.filter { !isPointInImage(it, imageWidth, imageHeight) }\n        \n        return if (invalidPoints.isEmpty()) {\n            ValidationResult(true, \"形状边界有效\")\n        } else {\n            ValidationResult(false, \"形状包含超出边界的点: $invalidPoints\")\n        }\n    }\n}\n"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/shapedrawing/EditorController.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.shapedrawing\n\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.snapshots.SnapshotStateMap\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.geometry.Rect\nimport androidx.compose.ui.geometry.Size\nimport androidx.compose.ui.graphics.Canvas\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.ImageBitmap\nimport androidx.compose.ui.graphics.Paint\nimport androidx.compose.ui.graphics.drawscope.CanvasDrawScope\nimport androidx.compose.ui.graphics.toAwtImage\nimport androidx.compose.ui.unit.Density\nimport androidx.compose.ui.unit.LayoutDirection\nimport cn.netdiscovery.monica.ui.controlpanel.shapedrawing.layer.ImageLayer\nimport cn.netdiscovery.monica.ui.controlpanel.shapedrawing.layer.Layer\nimport cn.netdiscovery.monica.ui.controlpanel.shapedrawing.layer.LayerManager\nimport cn.netdiscovery.monica.ui.controlpanel.shapedrawing.layer.LayerRenderer\nimport cn.netdiscovery.monica.ui.controlpanel.shapedrawing.layer.LayerTransform\nimport cn.netdiscovery.monica.ui.controlpanel.shapedrawing.layer.LayerType\nimport cn.netdiscovery.monica.ui.controlpanel.shapedrawing.layer.ShapeLayer\nimport cn.netdiscovery.monica.ui.controlpanel.shapedrawing.layer.SpecialLayerHelper\nimport cn.netdiscovery.monica.ui.controlpanel.shapedrawing.model.Shape\nimport java.awt.image.BufferedImage\nimport java.util.UUID\nimport org.slf4j.Logger\nimport org.slf4j.LoggerFactory\n\n/**\n * EditorController 负责协调 LayerManager、LayerRenderer 以及导出逻辑，\n * 同时记录当前工具与激活图层信息，供 UI 直接调用。\n */\nclass EditorController(\n    val layerManager: LayerManager = LayerManager()\n) {\n    \n    private val logger: Logger = LoggerFactory.getLogger(EditorController::class.java)\n    \n    // 使用 SpecialLayerHelper 来管理背景层，集中处理背景层相关逻辑\n    private val specialLayerHelper = SpecialLayerHelper(layerManager, BACKGROUND_LAYER_NAME)\n\n    companion object {\n        /**\n         * 限制最多创建的形状层数量\n         * 方案一（简化设计）：限制为1个形状层，保留多个图像层\n         */\n        private const val MAX_SHAPE_LAYERS = 1\n        \n        /**\n         * 背景图层名称常量，统一管理，避免硬编码\n         */\n        const val BACKGROUND_LAYER_NAME = \"背景图层\"\n    }\n\n    val layerRenderer = LayerRenderer(layerManager)\n\n    private val _currentTool = mutableStateOf(EditorTool.SELECTION)\n    val currentTool get() = _currentTool.value\n\n    fun selectTool(tool: EditorTool) {\n        _currentTool.value = tool\n    }\n\n    fun addLayer(layer: Layer, index: Int? = null) {\n        layerManager.addLayer(layer, index)\n    }\n\n    fun createImageLayer(\n        name: String,\n        image: ImageBitmap?,\n        index: Int? = null\n    ): ImageLayer {\n        val layer = ImageLayer(name = name, image = image)\n        layerManager.addLayer(layer, index)\n        return layer\n    }\n\n    /**\n     * 添加形状层，但限制最多只能创建 MAX_SHAPE_LAYERS 个形状层\n     * 如果已达上限，返回现有的第一个形状层并激活它\n     */\n    fun addShapeLayer(name: String = \"形状图层\"): ShapeLayer? {\n        val existingShapeLayers = layerManager.layers.value.filter { it.type == LayerType.SHAPE }\n\n        if (existingShapeLayers.size >= MAX_SHAPE_LAYERS) {\n            // 如果已达上限，返回现有的第一个形状层并激活它\n            val existing = existingShapeLayers.firstOrNull() as? ShapeLayer\n            existing?.let { layerManager.setActiveLayer(it.id) }\n            return existing\n        }\n\n        val layer = ShapeLayer(name)\n        layerManager.addLayer(layer)\n        return layer\n    }\n\n    /**\n     * 获取当前形状层数量\n     */\n    fun getShapeLayerCount(): Int {\n        return layerManager.layers.value.count { it.type == LayerType.SHAPE }\n    }\n\n    /**\n     * 检查是否可以添加更多形状层\n     */\n    fun canAddShapeLayer(): Boolean {\n        return getShapeLayerCount() < MAX_SHAPE_LAYERS\n    }\n\n    fun removeLayer(id: UUID) {\n        if (isBackgroundLayer(id)) {\n            // 背景层不应该被删除，记录警告但不执行删除\n            logger.warn(\"尝试删除背景层，操作被阻止\")\n            return\n        }\n        layerManager.removeLayer(id)\n    }\n\n    fun clearLayers() {\n        layerManager.clear()\n    }\n\n    fun setActiveLayer(id: UUID?) {\n        layerManager.setActiveLayer(id)\n    }\n\n    fun ensureActiveShapeLayer(): ShapeLayer {\n        val active = layerManager.activeLayer.value\n        if (active is ShapeLayer) return active\n\n        val existing = layerManager.layers.value.firstOrNull { it.type == LayerType.SHAPE } as? ShapeLayer\n        if (existing != null) {\n            layerManager.setActiveLayer(existing.id)\n            return existing\n        }\n\n        val newLayer = ShapeLayer(\"Shape Layer\")\n        layerManager.addLayer(newLayer)\n        return newLayer\n    }\n\n    /**\n     * 检查是否可以在当前激活的形状层上绘制\n     * 返回 true 表示可以绘制，false 表示图层锁定或不是形状层\n     * 注意：此方法会先确保存在一个激活的形状层，然后再检查锁定状态\n     */\n    fun canDrawOnActiveShapeLayer(): Boolean {\n        // 先确保有一个激活的形状层\n        val shapeLayer = try {\n            ensureActiveShapeLayer()\n        } catch (e: Exception) {\n            return false\n        }\n        // 检查是否锁定\n        return !shapeLayer.locked\n    }\n\n    fun addShapeToActiveLayer(key: Offset, displayShape: Shape, originalShape: Shape) {\n        val shapeLayer = ensureActiveShapeLayer()\n        // 如果形状层已锁定，不允许添加形状\n        if (shapeLayer.locked) {\n            return\n        }\n        shapeLayer.addShape(key, displayShape, originalShape)\n    }\n\n    fun replaceShapesInActiveLayer(\n        displayLines: SnapshotStateMap<Offset, Shape.Line>,\n        originalLines: SnapshotStateMap<Offset, Shape.Line>,\n        displayCircles: SnapshotStateMap<Offset, Shape.Circle>,\n        originalCircles: SnapshotStateMap<Offset, Shape.Circle>,\n        displayTriangles: SnapshotStateMap<Offset, Shape.Triangle>,\n        originalTriangles: SnapshotStateMap<Offset, Shape.Triangle>,\n        displayRectangles: SnapshotStateMap<Offset, Shape.Rectangle>,\n        originalRectangles: SnapshotStateMap<Offset, Shape.Rectangle>,\n        displayPolygons: SnapshotStateMap<Offset, Shape.Polygon>,\n        originalPolygons: SnapshotStateMap<Offset, Shape.Polygon>,\n        displayTexts: SnapshotStateMap<Offset, Shape.Text>,\n        originalTexts: SnapshotStateMap<Offset, Shape.Text>\n    ) {\n        val shapeLayer = ensureActiveShapeLayer()\n        // 如果形状层已锁定，不允许替换形状\n        if (shapeLayer.locked) {\n            return\n        }\n        shapeLayer.replaceAll(\n            displayLines,\n            originalLines,\n            displayCircles,\n            originalCircles,\n            displayTriangles,\n            originalTriangles,\n            displayRectangles,\n            originalRectangles,\n            displayPolygons,\n            originalPolygons,\n            displayTexts,\n            originalTexts\n        )\n    }\n\n    /**\n     * 将当前所有图层合成为 [androidx.compose.ui.graphics.ImageBitmap]。\n     *\n     * @param width 导出宽度（像素）\n     * @param height 导出高度（像素）\n     * @param density 当前绘制使用的密度\n     * @param backgroundColor 可选背景色，默认为透明\n     * @param layers 指定要导出的图层集合，默认为 LayerManager 当前图层快照\n     */\n    fun exportImageBitmap(\n        width: Int,\n        height: Int,\n        density: Density,\n        backgroundColor: Color = Color.Transparent,\n        layers: List<Layer> = layerManager.layers.value\n    ): ImageBitmap {\n        val bitmap = ImageBitmap(width, height)\n        val canvas = Canvas(bitmap)\n        val drawScope = CanvasDrawScope()\n        val size = Size(width.toFloat(), height.toFloat())\n\n        drawScope.draw(\n            density = density,\n            layoutDirection = LayoutDirection.Ltr,\n            canvas = canvas,\n            size = size\n        ) {\n            if (backgroundColor.alpha > 0f) {\n                val rect = Rect(Offset.Zero, size)\n                val paint = Paint().apply {\n                    color = backgroundColor\n                }\n                drawContext.canvas.drawRect(rect, paint)\n            }\n            layerRenderer.drawAll(this, layers)\n        }\n\n        return bitmap\n    }\n\n    /**\n     * 将当前所有图层合成为 [java.awt.image.BufferedImage]。\n     *\n     * @param width 导出宽度（像素）\n     * @param height 导出高度（像素）\n     * @param density 当前绘制使用的密度\n     * @param backgroundColor 可选背景色，默认为透明\n     * @param layers 指定要导出的图层集合，默认为 LayerManager 当前图层快照\n     */\n    fun exportBufferedImage(\n        width: Int,\n        height: Int,\n        density: Density,\n        backgroundColor: Color = Color.Transparent,\n        layers: List<Layer> = layerManager.layers.value\n    ): BufferedImage {\n        val bitmap = exportImageBitmap(width, height, density, backgroundColor, layers)\n        return bitmap.toAwtImage()\n    }\n\n    /**\n     * 更新图像层的位置（拖动）\n     */\n    /**\n     * 更新图像层的位置\n     * @param layerId 图层ID\n     * @param canvasPosition 画布坐标位置（用户拖动的目标位置）\n     * @param canvasWidth 画布宽度\n     * @param canvasHeight 画布高度\n     */\n    fun updateImageLayerPosition(layerId: UUID, canvasPosition: Offset, canvasWidth: Float, canvasHeight: Float) {\n        val layer = layerManager.getLayerById(layerId) as? ImageLayer\n        layer?.let {\n            val bitmap = it.image ?: return\n            \n            // 计算适应和居中后的尺寸\n            val scaleX = canvasWidth / bitmap.width\n            val scaleY = canvasHeight / bitmap.height\n            val fitScale = minOf(scaleX, scaleY).coerceAtMost(1f)\n            \n            val scaledWidth = bitmap.width * fitScale\n            val scaledHeight = bitmap.height * fitScale\n            \n            val centerOffsetX = (canvasWidth - scaledWidth) / 2f\n            val centerOffsetY = (canvasHeight - scaledHeight) / 2f\n            \n            // 适应后图像的中心点（在画布坐标系中，不考虑用户平移）\n            val adaptedImageCenter = Offset(\n                centerOffsetX + scaledWidth / 2f,\n                centerOffsetY + scaledHeight / 2f\n            )\n            \n            // 计算画布坐标中的偏移（用户想要移动到的位置相对于适应后图像中心的偏移）\n            val canvasOffset = canvasPosition - adaptedImageCenter\n            \n            // 将画布坐标的偏移转换为图像原始坐标系中的偏移\n            // 因为在 withTransform 中，translation 是在 fitScale 之前应用的\n            // 所以 translation 应该在图像原始坐标系中\n            // 变换顺序：用户平移(translation) -> fitScale -> centerOffset\n            // 因此：canvasOffset = translation * fitScale\n            // 所以：translation = canvasOffset / fitScale\n            val translation = Offset(\n                canvasOffset.x / fitScale,\n                canvasOffset.y / fitScale\n            )\n            \n            val currentTransform = it.transform\n            it.updateTransform(\n                currentTransform.copy(translation = translation)\n            )\n        }\n    }\n    \n    /**\n     * 更新图像层的位置（使用相对偏移）\n     * @param layerId 图层ID\n     * @param translation 相对于适应后图像中心的偏移（在适应后的坐标系中）\n     */\n    fun updateImageLayerPosition(layerId: UUID, translation: Offset) {\n        val layer = layerManager.getLayerById(layerId) as? ImageLayer\n        layer?.let {\n            val currentTransform = it.transform\n            it.updateTransform(\n                currentTransform.copy(translation = translation)\n            )\n        }\n    }\n\n    /**\n     * 更新图像层的旋转角度\n     */\n    fun updateImageLayerRotation(layerId: UUID, rotation: Float, pivot: Offset) {\n        val layer = layerManager.getLayerById(layerId) as? ImageLayer\n        layer?.let {\n            val currentTransform = it.transform\n            it.updateTransform(\n                currentTransform.copy(rotation = rotation, pivot = pivot)\n            )\n        }\n    }\n\n    /**\n     * 更新图像层的缩放比例\n     */\n    fun updateImageLayerScale(layerId: UUID, scaleX: Float, scaleY: Float, pivot: Offset) {\n        val layer = layerManager.getLayerById(layerId) as? ImageLayer\n        layer?.let {\n            val currentTransform = it.transform\n            it.updateTransform(\n                currentTransform.copy(scaleX = scaleX, scaleY = scaleY, pivot = pivot)\n            )\n        }\n    }\n\n    /**\n     * 更新图像层的完整变换\n     */\n    fun updateImageLayerTransform(layerId: UUID, transform: LayerTransform) {\n        val layer = layerManager.getLayerById(layerId) as? ImageLayer\n        layer?.let {\n            it.updateTransform(transform)\n        }\n    }\n    \n    /**\n     * 更新图像层的裁剪区域\n     * @param layerId 图层ID\n     * @param cropRect 裁剪区域（在图像坐标系中，null表示取消裁剪）\n     */\n    fun updateImageLayerCrop(layerId: UUID, cropRect: Rect?) {\n        val layer = layerManager.getLayerById(layerId) as? ImageLayer\n        layer?.let {\n            val currentTransform = it.transform\n            it.updateTransform(\n                currentTransform.copy(cropRect = cropRect)\n            )\n        }\n    }\n\n    /**\n     * 获取当前激活的图像层\n     */\n    fun getActiveImageLayer(): ImageLayer? {\n        val active = layerManager.activeLayer.value\n        return active as? ImageLayer\n    }\n    \n    // ==================== 背景层管理方法 ====================\n    \n    /**\n     * 检查指定图层是否为背景层\n     */\n    fun isBackgroundLayer(layer: Layer): Boolean {\n        return layer.name == BACKGROUND_LAYER_NAME && layer is ImageLayer\n    }\n    \n    /**\n     * 检查指定图层是否为背景层（通过 ID）\n     */\n    fun isBackgroundLayer(layerId: UUID): Boolean {\n        val layer = layerManager.getLayerById(layerId)\n        return layer != null && isBackgroundLayer(layer)\n    }\n    \n    /**\n     * 获取背景层，如果不存在则返回 null\n     */\n    fun getBackgroundLayer(): ImageLayer? {\n        return specialLayerHelper.getBackgroundLayer()\n    }\n    \n    /**\n     * 获取或创建背景层\n     * 如果不存在则创建新的背景层并添加到索引 0\n     */\n    fun getOrCreateBackgroundLayer(image: ImageBitmap): ImageLayer {\n        return specialLayerHelper.getOrCreateBackgroundLayer(image)\n    }\n    \n    /**\n     * 更新背景层图像\n     * 如果背景层不存在，则创建新的\n     */\n    fun updateBackgroundLayer(image: ImageBitmap) {\n        specialLayerHelper.updateBackgroundLayer(image)\n    }\n    \n    /**\n     * 检查是否存在背景层\n     */\n    fun hasBackgroundLayer(): Boolean {\n        return specialLayerHelper.hasBackgroundLayer()\n    }\n    \n    /**\n     * 移除背景层（谨慎使用，通常不应该删除背景层）\n     * 此方法主要用于清理或重置场景\n     */\n    fun removeBackgroundLayer(): Boolean {\n        return specialLayerHelper.removeBackgroundLayer()\n    }\n    \n    /**\n     * 获取背景层的尺寸（如果存在）\n     */\n    fun getBackgroundSize(): Pair<Float, Float>? {\n        return specialLayerHelper.getBackgroundSize()\n    }\n    \n    /**\n     * 将图层上移一层（防止移动背景层）\n     */\n    fun moveLayerUp(layerId: UUID): Boolean {\n        if (isBackgroundLayer(layerId)) {\n            logger.warn(\"尝试移动背景层，操作被阻止\")\n            return false\n        }\n        return layerManager.moveLayerUp(layerId)\n    }\n    \n    /**\n     * 将图层下移一层（防止移动背景层）\n     */\n    fun moveLayerDown(layerId: UUID): Boolean {\n        if (isBackgroundLayer(layerId)) {\n            logger.warn(\"尝试移动背景层，操作被阻止\")\n            return false\n        }\n        return layerManager.moveLayerDown(layerId)\n    }\n}\n\nenum class EditorTool {\n    SELECTION,\n    SHAPE,\n    IMAGE,\n    MOVE\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/shapedrawing/ShapeDrawingView.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.shapedrawing\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.runtime.*\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.shadow\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.toComposeImageBitmap\nimport androidx.compose.ui.input.pointer.PointerButton\nimport androidx.compose.ui.input.pointer.pointerInput\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.material.MaterialTheme\nimport cn.netdiscovery.monica.state.ApplicationState\nimport cn.netdiscovery.monica.ui.controlpanel.shapedrawing.layer.ImageLayer\nimport cn.netdiscovery.monica.ui.controlpanel.shapedrawing.animation.ShapeAnimationManager\nimport cn.netdiscovery.monica.ui.controlpanel.shapedrawing.coordinate.CoordinateConverter\nimport cn.netdiscovery.monica.ui.controlpanel.shapedrawing.handler.ShapeDrawingEventHandler\nimport cn.netdiscovery.monica.ui.controlpanel.shapedrawing.model.ShapeEnum\nimport cn.netdiscovery.monica.ui.controlpanel.shapedrawing.model.ShapeProperties\nimport cn.netdiscovery.monica.ui.controlpanel.shapedrawing.state.ShapeDrawingState\nimport cn.netdiscovery.monica.ui.controlpanel.shapedrawing.widget.draggableTextField\nimport cn.netdiscovery.monica.ui.controlpanel.shapedrawing.widget.ShapeDrawingPropertiesMenuDialog\nimport cn.netdiscovery.monica.ui.controlpanel.shapedrawing.widget.CanvasView\nimport cn.netdiscovery.monica.ui.controlpanel.shapedrawing.widget.LayerPanel\nimport cn.netdiscovery.monica.ui.controlpanel.shapedrawing.widget.ControlPoint\nimport cn.netdiscovery.monica.ui.controlpanel.shapedrawing.widget.ControlPointType\nimport cn.netdiscovery.monica.ui.controlpanel.shapedrawing.widget.ImageLayerControlRenderer\nimport cn.netdiscovery.monica.ui.widget.color.ColorSelectionDialog\nimport cn.netdiscovery.monica.ui.widget.image.gesture.detectTransformGestures\nimport cn.netdiscovery.monica.ui.widget.image.gesture.dragMotionEvent\nimport cn.netdiscovery.monica.ui.widget.rightSideMenuBar\nimport cn.netdiscovery.monica.ui.widget.toolTipButton\nimport cn.netdiscovery.monica.ui.widget.image.ImageSizeCalculator\nimport cn.netdiscovery.monica.i18n.getCurrentStringResource\nimport org.slf4j.Logger\nimport org.slf4j.LoggerFactory\nimport kotlin.math.atan2\n\n/**\n * 重构后的形状绘制视图\n * 通过模块化设计降低耦合度，提高可维护性\n * 实现模式一：绘制完成后颜色不变\n * \n * @author Tony Shen\n * @date 2024/12/19\n * @version V1.0\n */\nprivate val logger: Logger = LoggerFactory.getLogger(object : Any() {}.javaClass.enclosingClass)\n\n@Composable\nfun shapeDrawing(state: ApplicationState) {\n    val density = LocalDensity.current\n    val i18nState = getCurrentStringResource()\n    val editorController = remember { EditorController() }\n\n    val drawingState = remember { ShapeDrawingState() }\n    val animationManager = remember { ShapeAnimationManager() }\n    \n    // 观察激活图层状态\n    val activeLayer by editorController.layerManager.activeLayer.collectAsState()\n\n    val coordinateConverter = remember(state.currentImage, density.density) {\n        val originalSize = ImageSizeCalculator.getImagePixelSize(state)\n        val displaySize = ImageSizeCalculator.getImageDisplayPixelSize(state, density.density)\n        val scaleX = if (originalSize != null && displaySize != null) {\n            originalSize.first.toFloat() / displaySize.first.toFloat()\n        } else 1f\n        val scaleY = if (originalSize != null && displaySize != null) {\n            originalSize.second.toFloat() / displaySize.second.toFloat()\n        } else 1f\n        CoordinateConverter(scaleX, scaleY)\n    }\n\n    val eventHandler = remember { ShapeDrawingEventHandler(drawingState, coordinateConverter) }\n\n    var showColorDialog by remember { mutableStateOf(false) }\n    var showPropertiesDialog by remember { mutableStateOf(false) }\n    var showDraggableTextField by remember { mutableStateOf(false) }\n\n    val imageBitmap = state.currentImage?.toComposeImageBitmap() ?: run {\n        logger.error(\"当前图像为空，无法进行绘制\")\n        return\n    }\n\n    fun syncShapeLayer() {\n        editorController.replaceShapesInActiveLayer(\n            drawingState.displayLines,\n            drawingState.originalLines,\n            drawingState.displayCircles,\n            drawingState.originalCircles,\n            drawingState.displayTriangles,\n            drawingState.originalTriangles,\n            drawingState.displayRectangles,\n            drawingState.originalRectangles,\n            drawingState.displayPolygons,\n            drawingState.originalPolygons,\n            drawingState.displayTexts,\n            drawingState.originalTexts\n        )\n    }\n\n    LaunchedEffect(imageBitmap) {\n        // 使用 EditorController 的统一方法管理背景层\n        editorController.updateBackgroundLayer(imageBitmap)\n    }\n\n    LaunchedEffect(Unit) {\n        editorController.ensureActiveShapeLayer()\n        editorController.selectTool(EditorTool.SHAPE)\n        syncShapeLayer()\n    }\n\n    val (width, height) = ImageSizeCalculator.calculateImageSize(state)\n    val displaySize = ImageSizeCalculator.getImageDisplayPixelSize(state, density.density)\n    val bitmapWidth = displaySize?.first ?: 0\n    val bitmapHeight = displaySize?.second ?: 0\n\n    if (bitmapWidth <= 0 || bitmapHeight <= 0) {\n        Box(\n            modifier = Modifier.fillMaxSize(),\n            contentAlignment = Alignment.Center\n        ) {\n            androidx.compose.material.Text(\n                text = \"请先加载图片\",\n                color = Color.Gray\n            )\n        }\n        return\n    }\n\n    val scrollState = rememberScrollState()\n\n    Box(\n        modifier = Modifier\n            .fillMaxSize()\n            .background(\n                brush = Brush.verticalGradient(\n                    colors = listOf(\n                        MaterialTheme.colors.background,\n                        MaterialTheme.colors.surface\n                    )\n                )\n            )\n    ) {\n        Row(\n            modifier = Modifier.fillMaxSize()\n        ) {\n            LayerPanel(\n                editorController = editorController,\n                state = state,\n                modifier = Modifier\n                    .width(240.dp)\n                    .fillMaxHeight()\n            )\n\n            Box(\n                modifier = Modifier\n                    .weight(1f)\n                    .fillMaxHeight(),\n                contentAlignment = Alignment.Center\n            ) {\n                Column(\n                    modifier = Modifier\n                        .fillMaxSize()\n                        .verticalScroll(scrollState),\n                    horizontalAlignment = Alignment.CenterHorizontally,\n                    verticalArrangement = Arrangement.Center\n                ) {\n                    // 图像层拖动状态\n                    var imageLayerDragStart by remember { mutableStateOf<Offset?>(null) }\n                    var imageLayerStartTranslation by remember { mutableStateOf<Offset>(Offset.Zero) }\n                    \n                    // 控制点交互状态\n                    var activeControlPoint by remember { mutableStateOf<ControlPoint?>(null) }\n                    var controlPointDragStart by remember { mutableStateOf<Offset?>(null) }\n                    var initialTransform by remember { mutableStateOf<cn.netdiscovery.monica.ui.controlpanel.shapedrawing.layer.LayerTransform?>(null) }\n                    \n                    val canvasModifier = Modifier\n                        .width(width)\n                        .height(height)\n                        .padding(8.dp)\n                        .shadow(1.dp)\n                        .background(Color.White)\n                        // 右键旋转（暂时移除滚轮缩放，后续可以添加）\n                        .pointerInput(activeLayer?.id, bitmapWidth, bitmapHeight, width, height) {\n                            val activeImageLayer = activeLayer as? ImageLayer\n                            val canvasWidth = with(density) { width.toPx() }\n                            val canvasHeight = with(density) { height.toPx() }\n                            if (activeImageLayer != null && !activeImageLayer.locked && \n                                !editorController.isBackgroundLayer(activeImageLayer)) {\n                                detectTransformGestures(\n                                    panZoomLock = true, // 锁定平移和缩放，只允许旋转\n                                    onGesture = { centroid, pan, zoom, rotation, mainPointer, _ ->\n                                        // 检查是否是右键（secondary button）\n                                        // 注意：PointerButton 在某些 Compose 版本中可能不可用，暂时允许所有旋转操作\n                                        // TODO: 添加右键检测逻辑\n                                        val currentTransform = activeImageLayer.transform\n                                        val newRotation = currentTransform.rotation + rotation\n                                        \n                                        // 计算中心点作为 pivot\n                                        val center = ImageLayerControlRenderer.calculateImageCenter(\n                                            activeImageLayer,\n                                            canvasWidth,\n                                            canvasHeight\n                                        ) ?: return@detectTransformGestures\n                                        \n                                        // pivot 应该在图像原始坐标系中，相对于图像中心\n                                        // 图像中心在原始坐标系中是 (width/2, height/2)\n                                        val imageBitmap = activeImageLayer.image\n                                        val imageCenter = if (imageBitmap != null) {\n                                            Offset(imageBitmap.width / 2f, imageBitmap.height / 2f)\n                                        } else {\n                                            Offset.Zero\n                                        }\n                                        \n                                        editorController.updateImageLayerRotation(\n                                            activeImageLayer.id,\n                                            newRotation,\n                                            imageCenter\n                                        )\n                                        mainPointer.consume()\n                                    }\n                                )\n                            }\n                        }\n                        .dragMotionEvent(\n                            onDragStart = { pointerInputChange ->\n                                val activeImageLayer = activeLayer as? ImageLayer\n                                \n                                // 检查是否点击在控制点上\n                                if (activeImageLayer != null && !activeImageLayer.locked && \n                                    !editorController.isBackgroundLayer(activeImageLayer)) {\n                                    val canvasWidth = with(density) { width.toPx() }\n                                    val canvasHeight = with(density) { height.toPx() }\n                                    val hitControlPoint = ImageLayerControlRenderer.hitTestControlPoint(\n                                        pointerInputChange.position,\n                                        activeImageLayer,\n                                        canvasWidth,\n                                        canvasHeight\n                                    )\n                                    \n                                    if (hitControlPoint != null) {\n                                        // 开始拖动控制点\n                                        activeControlPoint = hitControlPoint\n                                        controlPointDragStart = pointerInputChange.position\n                                        initialTransform = activeImageLayer.transform.copy()\n                                        pointerInputChange.consume()\n                                        return@dragMotionEvent\n                                    }\n                                }\n                                \n                                // 如果激活图层是图像层且未锁定，则直接拖动图像层\n                                if (activeImageLayer != null && !activeImageLayer.locked) {\n                                    imageLayerDragStart = pointerInputChange.position\n                                    imageLayerStartTranslation = activeImageLayer.transform.translation\n                                    pointerInputChange.consume()\n                                    return@dragMotionEvent\n                                }\n                                \n                                // 如果激活图层是形状层，检查是否锁定\n                                if (!editorController.canDrawOnActiveShapeLayer()) {\n                                    state.showTray(\"形状层已锁定，无法绘制\", \"提示\")\n                                    pointerInputChange.consume()\n                                    return@dragMotionEvent\n                                }\n                                eventHandler.handleMouseDown(pointerInputChange.position)\n                                pointerInputChange.consume()\n                            },\n                            onDrag = { pointerInputChange ->\n                                val activeImageLayer = activeLayer as? ImageLayer\n                                \n                                // 如果正在拖动控制点\n                                if (activeImageLayer != null && activeControlPoint != null && controlPointDragStart != null && initialTransform != null) {\n                                    val currentPos = pointerInputChange.position\n                                    val startPos = controlPointDragStart!!\n                                    val controlPoint = activeControlPoint!!\n                                    \n                                    when (controlPoint.type) {\n                                        ControlPointType.ROTATION_HANDLE -> {\n                                            // 拖动旋转手柄进行旋转\n                                            val canvasWidth = with(density) { width.toPx() }\n                                            val canvasHeight = with(density) { height.toPx() }\n                                            val center = ImageLayerControlRenderer.calculateImageCenter(\n                                                activeImageLayer,\n                                                canvasWidth,\n                                                canvasHeight\n                                            ) ?: return@dragMotionEvent\n                                            \n                                            val startAngle = atan2(\n                                                startPos.y - center.y,\n                                                startPos.x - center.x\n                                            )\n                                            val currentAngle = atan2(\n                                                currentPos.y - center.y,\n                                                currentPos.x - center.x\n                                            )\n                                            val rotationDelta = Math.toDegrees((currentAngle - startAngle).toDouble()).toFloat()\n                                            \n                                            // pivot 应该在图像原始坐标系中，相对于图像中心\n                                            // 图像中心在原始坐标系中是 (width/2, height/2)\n                                            val imageBitmap = activeImageLayer.image\n                                            val imageCenter = if (imageBitmap != null) {\n                                                Offset(imageBitmap.width / 2f, imageBitmap.height / 2f)\n                                            } else {\n                                                Offset.Zero\n                                            }\n                                            val newRotation = initialTransform!!.rotation + rotationDelta\n                                            \n                                            editorController.updateImageLayerRotation(\n                                                activeImageLayer.id,\n                                                newRotation,\n                                                imageCenter\n                                            )\n                                        }\n                                        ControlPointType.CORNER_TOP_LEFT,\n                                        ControlPointType.CORNER_TOP_RIGHT,\n                                        ControlPointType.CORNER_BOTTOM_LEFT,\n                                        ControlPointType.CORNER_BOTTOM_RIGHT -> {\n                                            // 拖动角点进行缩放\n                                            val canvasWidth = with(density) { width.toPx() }\n                                            val canvasHeight = with(density) { height.toPx() }\n                                            val center = ImageLayerControlRenderer.calculateImageCenter(\n                                                activeImageLayer,\n                                                canvasWidth,\n                                                canvasHeight\n                                            ) ?: return@dragMotionEvent\n                                            \n                                            val startDistance = (startPos - center).getDistance()\n                                            val currentDistance = (currentPos - center).getDistance()\n                                            \n                                            if (startDistance > 0f) {\n                                                val scaleFactor = currentDistance / startDistance\n                                                val newScaleX = (initialTransform!!.scaleX * scaleFactor).coerceIn(0.1f, 10f)\n                                                val newScaleY = (initialTransform!!.scaleY * scaleFactor).coerceIn(0.1f, 10f)\n                                                \n                                                // pivot 应该在图像原始坐标系中，相对于图像中心\n                                                // 图像中心在原始坐标系中是 (width/2, height/2)\n                                                val imageBitmap = activeImageLayer.image\n                                                val imageCenter = if (imageBitmap != null) {\n                                                    Offset(imageBitmap.width / 2f, imageBitmap.height / 2f)\n                                                } else {\n                                                    Offset.Zero\n                                                }\n                                                \n                                                editorController.updateImageLayerScale(\n                                                    activeImageLayer.id,\n                                                    newScaleX,\n                                                    newScaleY,\n                                                    imageCenter\n                                                )\n                                            }\n                                        }\n                                        // 裁剪控制点暂时不处理，后续可以添加\n                                        ControlPointType.CROP_TOP_LEFT,\n                                        ControlPointType.CROP_TOP_RIGHT,\n                                        ControlPointType.CROP_BOTTOM_LEFT,\n                                        ControlPointType.CROP_BOTTOM_RIGHT,\n                                        ControlPointType.CROP_TOP,\n                                        ControlPointType.CROP_BOTTOM,\n                                        ControlPointType.CROP_LEFT,\n                                        ControlPointType.CROP_RIGHT -> {\n                                            // TODO: 实现裁剪控制点的拖动逻辑\n                                        }\n                                    }\n                                    \n                                    pointerInputChange.consume()\n                                    return@dragMotionEvent\n                                }\n                                \n                                // 如果正在拖动图像层\n                                if (activeImageLayer != null && imageLayerDragStart != null && !activeImageLayer.locked) {\n                                    val canvasWidth = with(density) { width.toPx() }\n                                    val canvasHeight = with(density) { height.toPx() }\n                                    // 使用新的方法，传入画布坐标和尺寸\n                                    editorController.updateImageLayerPosition(\n                                        activeImageLayer.id,\n                                        pointerInputChange.position,\n                                        canvasWidth,\n                                        canvasHeight\n                                    )\n                                    pointerInputChange.consume()\n                                    return@dragMotionEvent\n                                }\n                                \n                                // 否则，处理形状绘制（仅在形状层激活时）\n                                if (!editorController.canDrawOnActiveShapeLayer()) {\n                                    pointerInputChange.consume()\n                                    return@dragMotionEvent\n                                }\n                                val currentShapes = eventHandler.handleMouseMove(pointerInputChange.position)\n                                currentShapes.forEach { (key, shape) ->\n                                    when (shape) {\n                                        is cn.netdiscovery.monica.ui.controlpanel.shapedrawing.model.Shape.Line -> drawingState.displayLines[key] = shape\n                                        is cn.netdiscovery.monica.ui.controlpanel.shapedrawing.model.Shape.Circle -> drawingState.displayCircles[key] = shape\n                                        is cn.netdiscovery.monica.ui.controlpanel.shapedrawing.model.Shape.Triangle -> drawingState.displayTriangles[key] = shape\n                                        is cn.netdiscovery.monica.ui.controlpanel.shapedrawing.model.Shape.Rectangle -> drawingState.displayRectangles[key] = shape\n                                        is cn.netdiscovery.monica.ui.controlpanel.shapedrawing.model.Shape.Polygon -> drawingState.displayPolygons[key] = shape\n                                        is cn.netdiscovery.monica.ui.controlpanel.shapedrawing.model.Shape.Text -> drawingState.displayTexts[key] = shape\n                                    }\n                                }\n                                syncShapeLayer()\n                                pointerInputChange.consume()\n                            },\n                            onDragEnd = { pointerInputChange ->\n                                val activeImageLayer = activeLayer as? ImageLayer\n                                \n                                // 如果正在拖动控制点，结束拖动\n                                if (activeImageLayer != null && activeControlPoint != null) {\n                                    activeControlPoint = null\n                                    controlPointDragStart = null\n                                    initialTransform = null\n                                    pointerInputChange.consume()\n                                    return@dragMotionEvent\n                                }\n                                \n                                // 如果正在拖动图像层，结束拖动\n                                if (activeImageLayer != null && imageLayerDragStart != null) {\n                                    imageLayerDragStart = null\n                                    pointerInputChange.consume()\n                                    return@dragMotionEvent\n                                }\n                                \n                                // 否则，处理形状绘制结束\n                                if (!editorController.canDrawOnActiveShapeLayer()) {\n                                    pointerInputChange.consume()\n                                    return@dragMotionEvent\n                                }\n                                val result = eventHandler.handleMouseUp(pointerInputChange.position, bitmapWidth, bitmapHeight)\n                                result?.let { (key, shape) ->\n                                    val shapeType = when (shape) {\n                                        is cn.netdiscovery.monica.ui.controlpanel.shapedrawing.model.Shape.Line -> \"Line\"\n                                        is cn.netdiscovery.monica.ui.controlpanel.shapedrawing.model.Shape.Circle -> \"Circle\"\n                                        is cn.netdiscovery.monica.ui.controlpanel.shapedrawing.model.Shape.Triangle -> \"Triangle\"\n                                        is cn.netdiscovery.monica.ui.controlpanel.shapedrawing.model.Shape.Rectangle -> \"Rectangle\"\n                                        is cn.netdiscovery.monica.ui.controlpanel.shapedrawing.model.Shape.Polygon -> \"Polygon\"\n                                        is cn.netdiscovery.monica.ui.controlpanel.shapedrawing.model.Shape.Text -> \"Text\"\n                                        else -> \"Unknown\"\n                                    }\n                                    animationManager.addAnimatedShape(shapeType, key)\n                                }\n                                syncShapeLayer()\n                                pointerInputChange.consume()\n                            }\n                        )\n\n                    CanvasView(\n                        editorController = editorController,\n                        drawingState = drawingState,\n                        animationManager = animationManager,\n                        modifier = canvasModifier\n                    )\n                }\n                \n                // 将 TextInputDialog 放在画布所在的 Box 中，使其相对于画布居中\n                if (showDraggableTextField) {\n                    // 计算画布的实际显示尺寸（像素），应该等于 bitmapWidth 和 bitmapHeight\n                    val canvasDisplayWidthPx = bitmapWidth.toFloat()\n                    val canvasDisplayHeightPx = bitmapHeight.toFloat()\n                    \n                    TextInputDialog(\n                        modifier = Modifier.width(250.dp).height(130.dp),\n                        canvasWidthPx = canvasDisplayWidthPx,\n                        canvasHeightPx = canvasDisplayHeightPx,\n                        density = density,\n                        currentText = drawingState.currentText,\n                        currentShapeProperty = drawingState.currentShapeProperty,\n                        onTextChanged = { drawingState.updateTextState(it) },\n                        onDragged = { offset ->\n                            // offset 是相对于画布中心的偏移（像素）\n                            // 由于画布显示尺寸等于图像显示尺寸，offset 可以直接使用\n                            val textPosition = CoordinateSystem.calculateTextPosition(\n                                dragOffset = offset,\n                                imageWidth = bitmapWidth,\n                                imageHeight = bitmapHeight,\n                                density = density,\n                                textFieldWidth = 250f,\n                                textFieldHeight = 130f,\n                                fontSize = drawingState.currentShapeProperty.fontSize\n                            )\n\n                            val textValidation = CoordinateSystem.validateOffset(textPosition, bitmapWidth, bitmapHeight)\n                            if (textValidation.isValid) {\n                                val displayText = cn.netdiscovery.monica.ui.controlpanel.shapedrawing.model.Shape.Text(\n                                    textPosition,\n                                    drawingState.currentText,\n                                    drawingState.currentShapeProperty\n                                )\n                                val originalText = coordinateConverter.convertTextToOriginal(displayText)\n                                drawingState.addShape(textPosition, displayText, originalText)\n                                drawingState.recordLastDrawnShape(textPosition, \"Text\")\n                                logger.info(\"添加文字: '${drawingState.currentText}' 在位置 $textPosition\")\n\n                                syncShapeLayer()\n                                drawingState.updateTextState(\"\")\n                            } else {\n                                logger.warn(\"文本位置无效: ${textValidation.message}\")\n                            }\n                            showDraggableTextField = false\n                        }\n                    )\n                }\n            }\n        }\n\n        rightSideMenuBar(modifier = Modifier.align(Alignment.CenterEnd)) {\n            \n            toolTipButton(\n                text = i18nState.get(\"select_color\"),\n                painter = painterResource(\"images/doodle/color.png\"),\n                onClick = { showColorDialog = true }\n            )\n\n            toolTipButton(\n                text = i18nState.get(\"change_properties\"),\n                painter = painterResource(\"images/doodle/brush.png\"),\n                onClick = { showPropertiesDialog = true }\n            )\n\n            ShapeSelectionButtons(drawingState)\n\n            toolTipButton(\n                text = i18nState.get(\"add_text\"),\n                painter = painterResource(\"images/shapedrawing/text.png\"),\n                onClick = { showDraggableTextField = true }\n            )\n\n            toolTipButton(\n                text = i18nState.get(\"clear\"),\n                painter = painterResource(\"images/doodle/clear.png\"),\n                onClick = {\n                    drawingState.clearAllShapes()\n                    animationManager.clearAllAnimations()\n                    syncShapeLayer()\n                }\n            )\n\n            toolTipButton(\n                text = i18nState.get(\"save\"),\n                painter = painterResource(\"images/doodle/save.png\"),\n                onClick = {\n                    // 使用显示尺寸而不是原始像素尺寸，确保导出和显示一致\n                    // 注意：Canvas 有 padding(8.dp)，所以实际绘制区域需要减去 padding\n                    val displaySize = ImageSizeCalculator.getImageDisplayPixelSize(state, density.density)\n                    val current = state.currentImage\n                    if (displaySize == null || current == null) {\n                        logger.warn(\"当前无法导出：缺少有效图像\")\n                        return@toolTipButton\n                    }\n                    // 计算减去 padding 后的实际绘制区域尺寸（Canvas 内部 drawScope.size）\n                    val paddingPx = with(density) { (8.dp * 2).toPx() } // 左右各 8.dp，上下各 8.dp\n                    val actualCanvasWidth = (displaySize.first - paddingPx).toInt().coerceAtLeast(1)\n                    val actualCanvasHeight = (displaySize.second - paddingPx).toInt().coerceAtLeast(1)\n                    \n                    val flattened = editorController.exportBufferedImage(\n                        width = actualCanvasWidth,\n                        height = actualCanvasHeight,\n                        density = density\n                    )\n                    state.addQueue(current)\n                    state.currentImage = flattened\n                    state.closePreviewWindow()\n                }\n            )\n        }\n\n        if (showColorDialog) {\n            ColorSelectionDialog(\n                drawingState.currentShapeProperty.color,\n                onDismiss = { showColorDialog = false },\n                onNegativeClick = { showColorDialog = false },\n                onPositiveClick = { color: Color ->\n                    showColorDialog = false\n                    drawingState.updateColor(color)\n                    logger.info(\"颜色已更改: ${color} (仅影响新绘制的形状)\")\n                }\n            )\n        }\n\n\n        if (showPropertiesDialog) {\n            ShapeDrawingPropertiesMenuDialog(drawingState.currentShapeProperty) { updatedProperties ->\n                drawingState.updateShapeProperty(updatedProperties)\n                logger.info(\"属性已更新: fontSize=${updatedProperties.fontSize}, alpha=${updatedProperties.alpha} (仅影响新绘制的形状)\")\n                showPropertiesDialog = false\n            }\n        }\n    }\n}\n\n/**\n * 形状选择按钮组\n */\n@Composable\nprivate fun ShapeSelectionButtons(drawingState: ShapeDrawingState) {\n    val i18nState = getCurrentStringResource()\n    val shapes = listOf(\n        Triple(ShapeEnum.Line, \"images/shapedrawing/line.png\", i18nState.get(\"line\")),\n        Triple(ShapeEnum.Circle, \"images/shapedrawing/circle.png\", i18nState.get(\"circle\")),\n        Triple(ShapeEnum.Triangle, \"images/shapedrawing/triangle.png\", i18nState.get(\"triangle\")),\n        Triple(ShapeEnum.Rectangle, \"images/shapedrawing/rectangle.png\", i18nState.get(\"rectangle\")),\n        Triple(ShapeEnum.Polygon, \"images/shapedrawing/polygon.png\", i18nState.get(\"polygon\"))\n    )\n    \n    shapes.forEach { (shape, icon, text) ->\n        toolTipButton(\n            text = text,\n            painter = painterResource(icon),\n            onClick = {\n                drawingState.selectShape(shape)\n            }\n        )\n    }\n}\n\n/**\n * 文字输入对话框组件\n */\n@Composable\nprivate fun TextInputDialog(\n    modifier: Modifier,\n    canvasWidthPx: Float,\n    canvasHeightPx: Float,\n    density: androidx.compose.ui.unit.Density,\n    currentText: String,\n    currentShapeProperty: ShapeProperties,\n    onTextChanged: (String) -> Unit,\n    onDragged: (Offset) -> Unit\n) {\n    draggableTextField(\n        modifier = modifier,\n        canvasWidthPx = canvasWidthPx,\n        canvasHeightPx = canvasHeightPx,\n        density = density,\n        text = currentText,\n        onTextChanged = onTextChanged,\n        onDragged = onDragged\n    )\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/shapedrawing/ShapeDrawingViewModel.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.shapedrawing\n\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.geometry.Size\nimport androidx.compose.ui.graphics.*\nimport androidx.compose.ui.graphics.drawscope.CanvasDrawScope\nimport androidx.compose.ui.unit.Density\nimport androidx.compose.ui.unit.LayoutDirection\nimport cn.netdiscovery.monica.state.ApplicationState\nimport cn.netdiscovery.monica.ui.controlpanel.shapedrawing.geometry.CanvasDrawer\nimport cn.netdiscovery.monica.ui.controlpanel.shapedrawing.geometry.Style\nimport cn.netdiscovery.monica.ui.controlpanel.shapedrawing.model.Shape.*\nimport cn.netdiscovery.monica.ui.controlpanel.shapedrawing.widget.TextDrawer\n\n/**\n * 形状绘制视图模型\n * 负责管理各种几何形状的绘制逻辑\n * \n * @author Tony Shen\n * @date 2024/11/21 16:09\n * @version V1.0\n */\nclass ShapeDrawingViewModel {\n\n    /**\n     * 绘制所有形状到画布\n     */\n    fun drawShape(\n        canvasDrawer: CanvasDrawer,\n        lines: Map<Offset, Line>,\n        circles: Map<Offset, Circle>,\n        triangles: Map<Offset, Triangle>,\n        rectangles: Map<Offset, Rectangle>,\n        polygons: Map<Offset, Polygon>,\n        texts: Map<Offset, Text>,\n        saveFlag: Boolean = false\n    ) {\n        drawLines(canvasDrawer, lines, saveFlag)\n        drawCircles(canvasDrawer, circles, saveFlag)\n        drawTriangles(canvasDrawer, triangles, saveFlag)\n        drawRectangles(canvasDrawer, rectangles, saveFlag)\n        drawPolygons(canvasDrawer, polygons, saveFlag)\n        drawTexts(canvasDrawer, texts)\n    }\n\n    /**\n     * 保存画布为位图\n     */\n    fun saveCanvasToBitmap(\n        density: Density,\n        lines: Map<Offset, Line>,\n        circles: Map<Offset, Circle>,\n        triangles: Map<Offset, Triangle>,\n        rectangles: Map<Offset, Rectangle>,\n        polygons: Map<Offset, Polygon>,\n        texts: Map<Offset, Text>,\n        image: ImageBitmap,\n        state: ApplicationState\n    ) {\n        val bitmapWidth = image.width\n        val bitmapHeight = image.height\n\n        val drawScope = CanvasDrawScope()\n        val size = Size(bitmapWidth.toFloat(), bitmapHeight.toFloat())\n        val canvas = Canvas(image)\n        val canvasDrawer = CanvasDrawer(TextDrawer, canvas)\n\n        drawScope.draw(\n            density = density,\n            layoutDirection = LayoutDirection.Ltr,\n            canvas = canvas,\n            size = size\n        ) {\n            state.closePreviewWindow()\n            drawShape(canvasDrawer, lines, circles, triangles, rectangles, polygons, texts, true)\n        }\n\n        state.addQueue(state.currentImage!!)\n        state.currentImage = image.toAwtImage()\n    }\n\n    // 绘制线条\n    private fun drawLines(canvasDrawer: CanvasDrawer, lines: Map<Offset, Line>, saveFlag: Boolean) {\n        lines.forEach { (_, line) ->\n            if (line.from != Offset.Unspecified && !saveFlag) {\n                canvasDrawer.point(line.from, line.shapeProperties.color)\n            }\n\n            if (line.from != Offset.Unspecified && line.to != Offset.Unspecified) {\n                val style = createStyle(line.shapeProperties)\n                canvasDrawer.line(line.from, line.to, style)\n            }\n        }\n    }\n\n    // 绘制圆形\n    private fun drawCircles(canvasDrawer: CanvasDrawer, circles: Map<Offset, Circle>, saveFlag: Boolean) {\n        circles.forEach { (_, circle) ->\n            if (circle.center != Offset.Unspecified && !saveFlag) {\n                canvasDrawer.point(circle.center, circle.shapeProperties.color)\n            }\n\n            if (circle.center != Offset.Unspecified) {\n                val style = createStyle(circle.shapeProperties)\n                canvasDrawer.circle(circle.center, circle.radius, style)\n            }\n        }\n    }\n\n    // 绘制三角形\n    private fun drawTriangles(canvasDrawer: CanvasDrawer, triangles: Map<Offset, Triangle>, saveFlag: Boolean) {\n        triangles.forEach { (_, triangle) ->\n            if (triangle.first != Offset.Unspecified && !saveFlag) {\n                canvasDrawer.point(triangle.first, triangle.shapeProperties.color)\n            }\n\n            val style = createStyle(triangle.shapeProperties)\n\n            // 绘制三角形的边\n            if (triangle.second != Offset.Unspecified && !saveFlag) {\n                canvasDrawer.point(triangle.second!!, triangle.shapeProperties.color)\n                canvasDrawer.line(triangle.first, triangle.second, style)\n            }\n\n            // 绘制完整的三角形\n            if (isValidTriangle(triangle)) {\n                val points = listOf(triangle.first, triangle.second!!, triangle.third!!)\n                canvasDrawer.polygon(points, style)\n            }\n        }\n    }\n\n    // 绘制矩形\n    private fun drawRectangles(canvasDrawer: CanvasDrawer, rectangles: Map<Offset, Rectangle>, saveFlag: Boolean) {\n        rectangles.forEach { (_, rect) ->\n            if (rect.rectFirst != Offset.Unspecified && !saveFlag) {\n                canvasDrawer.point(rect.rectFirst, rect.shapeProperties.color)\n            }\n\n            if (isValidRectangle(rect)) {\n                val points = listOf(rect.tl, rect.bl, rect.br, rect.tr)\n                val style = createStyle(rect.shapeProperties)\n                canvasDrawer.polygon(points, style)\n            }\n        }\n    }\n\n    // 绘制多边形\n    private fun drawPolygons(canvasDrawer: CanvasDrawer, polygons: Map<Offset, Polygon>, saveFlag: Boolean) {\n        polygons.forEach { (_, polygon) ->\n            if (polygon.points.isNotEmpty()) {\n                if (polygon.points[0] != Offset.Unspecified && !saveFlag) {\n                    canvasDrawer.point(polygon.points[0], polygon.shapeProperties.color)\n                }\n\n                val style = createStyle(polygon.shapeProperties)\n                \n                // 绘制多边形的边\n                if (polygon.points.size > 1 && polygon.points[1] != Offset.Unspecified && !saveFlag) {\n                    canvasDrawer.point(polygon.points[1], polygon.shapeProperties.color)\n                    canvasDrawer.line(polygon.points[0], polygon.points[1], style)\n                }\n\n                canvasDrawer.polygon(polygon.points, style)\n            }\n        }\n    }\n\n    // 绘制文本\n    private fun drawTexts(canvasDrawer: CanvasDrawer, texts: Map<Offset, Text>) {\n        texts.forEach { (_, text) ->\n            if (text.point != Offset.Unspecified) {\n                val textList = listOf(text.message)\n                canvasDrawer.text(text.point, textList, text.shapeProperties.color, text.shapeProperties.fontSize)\n            }\n        }\n    }\n\n    // 创建样式对象\n    private fun createStyle(properties: cn.netdiscovery.monica.ui.controlpanel.shapedrawing.model.ShapeProperties): Style {\n        return Style(\n            name = null,\n            color = properties.color,\n            border = properties.border,\n            equalityGroup = null,\n            fill = properties.fill,\n            scale = 1f,\n            alpha = properties.alpha,\n            bounded = true\n        )\n    }\n\n    // 验证三角形是否有效\n    private fun isValidTriangle(triangle: Triangle): Boolean {\n        return triangle.first != Offset.Unspecified && \n               triangle.second != Offset.Unspecified && \n               triangle.third != Offset.Unspecified\n    }\n\n    // 验证矩形是否有效\n    private fun isValidRectangle(rect: Rectangle): Boolean {\n        return rect.tl != Offset.Unspecified && \n               rect.bl != Offset.Unspecified && \n               rect.br != Offset.Unspecified && \n               rect.tr != Offset.Unspecified\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/shapedrawing/animation/ShapeAnimationManager.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.shapedrawing.animation\n\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.drawscope.DrawScope\nimport androidx.compose.ui.graphics.drawscope.Stroke\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.delay\nimport org.slf4j.Logger\nimport org.slf4j.LoggerFactory\n\n/**\n * 动画形状数据类\n */\ndata class AnimatedShape(\n    val key: String,\n    val shapeType: String,\n    val startTime: Long,\n    val duration: Long = 800L,\n    val startScale: Float = 0.3f,\n    val endScale: Float = 1.2f,\n    val startAlpha: Float = 0.1f,\n    val endAlpha: Float = 0.8f,\n    val highlightColor: Color = Color.Cyan,\n    val pulseEffect: Boolean = true\n)\n\n/**\n * 形状动画管理器\n * 负责管理形状绘制时的动画效果\n * \n * @author Tony Shen\n * @date 2025/9/8\n * @version V1.0\n */\nclass ShapeAnimationManager {\n    \n    private val logger: Logger = LoggerFactory.getLogger(this::class.java)\n    \n    // 动画状态管理\n    var animatedShapes by mutableStateOf<Map<String, AnimatedShape>>(emptyMap())\n        private set\n    \n    /**\n     * 添加动画形状\n     */\n    fun addAnimatedShape(shapeType: String, key: Offset) {\n        // 检查 key 是否有效\n        if (key == Offset.Unspecified) {\n            logger.warn(\"无法添加动画形状: key 是 Offset.Unspecified\")\n            return\n        }\n        \n        val shapeKey = \"${shapeType}_${key.x}_${key.y}\"\n        val currentTime = System.currentTimeMillis()\n        \n        // 根据形状类型设置不同的动画参数\n        val animationParams = when (shapeType) {\n            \"Circle\" -> AnimatedShape(\n                key = shapeKey,\n                shapeType = shapeType,\n                startTime = currentTime,\n                duration = 1000L,\n                highlightColor = Color.Cyan\n            )\n            \"Line\" -> AnimatedShape(\n                key = shapeKey,\n                shapeType = shapeType,\n                startTime = currentTime,\n                duration = 600L,\n                highlightColor = Color.Magenta\n            )\n            \"Triangle\" -> AnimatedShape(\n                key = shapeKey,\n                shapeType = shapeType,\n                startTime = currentTime,\n                duration = 800L,\n                highlightColor = Color.Green\n            )\n            \"Rectangle\" -> AnimatedShape(\n                key = shapeKey,\n                shapeType = shapeType,\n                startTime = currentTime,\n                duration = 700L,\n                highlightColor = Color.Blue\n            )\n            else -> AnimatedShape(\n                key = shapeKey,\n                shapeType = shapeType,\n                startTime = currentTime,\n                highlightColor = Color.Yellow\n            )\n        }\n        \n        animatedShapes = animatedShapes + (shapeKey to animationParams)\n        \n        // 动画结束后自动移除\n        kotlinx.coroutines.CoroutineScope(kotlinx.coroutines.Dispatchers.Default).launch {\n            delay(animationParams.duration + 200)\n            animatedShapes = animatedShapes - shapeKey\n        }\n        \n        logger.info(\"添加动画形状: $shapeType\")\n    }\n    \n    /**\n     * 缓动插值函数\n     */\n    fun lerp(start: Float, end: Float, fraction: Float): Float {\n        return start + (end - start) * fraction\n    }\n    \n    /**\n     * 缓动函数 - 缓入缓出\n     */\n    fun easeInOutCubic(t: Float): Float {\n        return if (t < 0.5f) {\n            4f * t * t * t\n        } else {\n            val temp = -2f * t + 2f\n            1f - (temp * temp * temp) / 2f\n        }\n    }\n    \n    /**\n     * 脉冲效果函数\n     */\n    fun pulseEffect(progress: Float): Float {\n        return (kotlin.math.sin((progress * kotlin.math.PI * 4).toDouble()).toFloat() + 1f) / 2f\n    }\n    \n    /**\n     * 清除所有动画\n     */\n    fun clearAllAnimations() {\n        animatedShapes = emptyMap()\n        logger.info(\"清除所有动画\")\n    }\n    \n    /**\n     * 绘制动画高亮效果\n     */\n    fun DrawScope.drawAnimationHighlight(\n        animatedShape: AnimatedShape,\n        scale: Float,\n        alpha: Float,\n        displayLines: Map<Offset, cn.netdiscovery.monica.ui.controlpanel.shapedrawing.model.Shape.Line>,\n        displayCircles: Map<Offset, cn.netdiscovery.monica.ui.controlpanel.shapedrawing.model.Shape.Circle>,\n        displayTriangles: Map<Offset, cn.netdiscovery.monica.ui.controlpanel.shapedrawing.model.Shape.Triangle>,\n        displayRectangles: Map<Offset, cn.netdiscovery.monica.ui.controlpanel.shapedrawing.model.Shape.Rectangle>,\n        displayPolygons: Map<Offset, cn.netdiscovery.monica.ui.controlpanel.shapedrawing.model.Shape.Polygon>\n    ) {\n        val key = animatedShape.key\n        val shapeType = animatedShape.shapeType\n        val highlightColor = animatedShape.highlightColor\n        \n        // 添加脉冲效果\n        val pulseAlpha = if (animatedShape.pulseEffect) {\n            alpha * (0.5f + 0.5f * pulseEffect(scale))\n        } else {\n            alpha\n        }\n        \n        // 根据形状类型获取位置和绘制动画效果\n        when (shapeType) {\n            \"Line\" -> {\n                val lineKey = Offset(\n                    key.split(\"_\")[1].toFloat(),\n                    key.split(\"_\")[2].toFloat()\n                )\n                displayLines[lineKey]?.let { line ->\n                    // 绘制多层效果\n                    drawLine(\n                        color = highlightColor.copy(alpha = pulseAlpha * 0.8f),\n                        start = line.from,\n                        end = line.to,\n                        strokeWidth = 12f * scale,\n                        cap = androidx.compose.ui.graphics.StrokeCap.Round\n                    )\n                    drawLine(\n                        color = Color.White.copy(alpha = pulseAlpha * 0.6f),\n                        start = line.from,\n                        end = line.to,\n                        strokeWidth = 6f * scale,\n                        cap = androidx.compose.ui.graphics.StrokeCap.Round\n                    )\n                }\n            }\n            \"Circle\" -> {\n                val circleKey = Offset(\n                    key.split(\"_\")[1].toFloat(),\n                    key.split(\"_\")[2].toFloat()\n                )\n                displayCircles[circleKey]?.let { circle ->\n                    // 绘制外圈和内圈\n                    drawCircle(\n                        color = highlightColor.copy(alpha = pulseAlpha * 0.4f),\n                        radius = circle.radius * scale * 1.2f,\n                        center = circle.center,\n                        style = Stroke(width = 4f * scale)\n                    )\n                    drawCircle(\n                        color = Color.White.copy(alpha = pulseAlpha * 0.6f),\n                        radius = circle.radius * scale,\n                        center = circle.center,\n                        style = Stroke(width = 2f * scale)\n                    )\n                }\n            }\n            \"Triangle\" -> {\n                val triangleKey = Offset(\n                    key.split(\"_\")[1].toFloat(),\n                    key.split(\"_\")[2].toFloat()\n                )\n                displayTriangles[triangleKey]?.let { triangle ->\n                    val path = androidx.compose.ui.graphics.Path().apply {\n                        moveTo(triangle.first.x, triangle.first.y)\n                        lineTo(triangle.second?.x ?: triangle.first.x, triangle.second?.y ?: triangle.first.y)\n                        lineTo(triangle.third?.x ?: triangle.first.x, triangle.third?.y ?: triangle.first.y)\n                        close()\n                    }\n                    drawPath(\n                        path = path,\n                        color = highlightColor.copy(alpha = pulseAlpha * 0.5f),\n                        style = Stroke(width = 8f * scale)\n                    )\n                    drawPath(\n                        path = path,\n                        color = Color.White.copy(alpha = pulseAlpha * 0.7f),\n                        style = Stroke(width = 3f * scale)\n                    )\n                }\n            }\n            \"Rectangle\" -> {\n                val rectKey = Offset(\n                    key.split(\"_\")[1].toFloat(),\n                    key.split(\"_\")[2].toFloat()\n                )\n                displayRectangles[rectKey]?.let { rect ->\n                    val rectSize = androidx.compose.ui.geometry.Size(\n                        rect.br.x - rect.tl.x,\n                        rect.br.y - rect.tl.y\n                    )\n                    drawRect(\n                        color = highlightColor.copy(alpha = pulseAlpha * 0.4f),\n                        topLeft = rect.tl,\n                        size = rectSize,\n                        style = Stroke(width = 6f * scale)\n                    )\n                    drawRect(\n                        color = Color.White.copy(alpha = pulseAlpha * 0.6f),\n                        topLeft = rect.tl,\n                        size = rectSize,\n                        style = Stroke(width = 2f * scale)\n                    )\n                }\n            }\n            \"Polygon\" -> {\n                val polygonKey = Offset(\n                    key.split(\"_\")[1].toFloat(),\n                    key.split(\"_\")[2].toFloat()\n                )\n                displayPolygons[polygonKey]?.let { polygon ->\n                    if (polygon.points.isNotEmpty()) {\n                        val path = androidx.compose.ui.graphics.Path().apply {\n                            moveTo(polygon.points[0].x, polygon.points[0].y)\n                            polygon.points.drop(1).forEach { point ->\n                                lineTo(point.x, point.y)\n                            }\n                            close()\n                        }\n                        drawPath(\n                            path = path,\n                            color = highlightColor.copy(alpha = pulseAlpha * 0.5f),\n                            style = Stroke(width = 8f * scale)\n                        )\n                        drawPath(\n                            path = path,\n                            color = Color.White.copy(alpha = pulseAlpha * 0.7f),\n                            style = Stroke(width = 3f * scale)\n                        )\n                    }\n                }\n            }\n        }\n    }\n    \n    /**\n     * 绘制所有动画效果\n     */\n    fun DrawScope.drawAllAnimations(\n        displayLines: Map<Offset, cn.netdiscovery.monica.ui.controlpanel.shapedrawing.model.Shape.Line>,\n        displayCircles: Map<Offset, cn.netdiscovery.monica.ui.controlpanel.shapedrawing.model.Shape.Circle>,\n        displayTriangles: Map<Offset, cn.netdiscovery.monica.ui.controlpanel.shapedrawing.model.Shape.Triangle>,\n        displayRectangles: Map<Offset, cn.netdiscovery.monica.ui.controlpanel.shapedrawing.model.Shape.Rectangle>,\n        displayPolygons: Map<Offset, cn.netdiscovery.monica.ui.controlpanel.shapedrawing.model.Shape.Polygon>\n    ) {\n        val currentTime = System.currentTimeMillis()\n        animatedShapes.forEach { (key, animatedShape) ->\n            val elapsed = currentTime - animatedShape.startTime\n            val rawProgress = (elapsed.toFloat() / animatedShape.duration).coerceIn(0f, 1f)\n            \n            if (rawProgress < 1f) {\n                // 使用缓动函数改进动画效果\n                val easedProgress = easeInOutCubic(rawProgress)\n                \n                val scale = lerp(animatedShape.startScale, animatedShape.endScale, easedProgress)\n                val alpha = lerp(animatedShape.startAlpha, animatedShape.endAlpha, easedProgress)\n                \n                // 绘制优化的动画高亮效果\n                drawAnimationHighlight(\n                    animatedShape, scale, alpha,\n                    displayLines, displayCircles, displayTriangles, displayRectangles, displayPolygons\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/shapedrawing/coordinate/CoordinateConverter.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.shapedrawing.coordinate\n\nimport androidx.compose.ui.geometry.Offset\nimport cn.netdiscovery.monica.ui.controlpanel.shapedrawing.model.Shape.*\n\n/**\n * 坐标转换器\n * 负责显示坐标和原始坐标之间的转换\n * \n * @author Tony Shen\n * @date 2025/9/8\n * @version V1.0\n */\nclass CoordinateConverter(private val scaleX: Float, private val scaleY: Float) {\n    \n    /**\n     * 显示坐标转原始坐标\n     */\n    fun displayToOriginal(displayOffset: Offset): Offset {\n        return Offset(displayOffset.x * scaleX, displayOffset.y * scaleY)\n    }\n    \n    /**\n     * 转换线段坐标\n     */\n    fun convertLineToOriginal(displayLine: Line): Line {\n        val originalFrom = displayToOriginal(displayLine.from)\n        val originalTo = displayToOriginal(displayLine.to)\n        return Line(originalFrom, originalTo, displayLine.shapeProperties)\n    }\n    \n    /**\n     * 转换圆形坐标\n     */\n    fun convertCircleToOriginal(displayCircle: Circle): Circle {\n        val originalCenter = displayToOriginal(displayCircle.center)\n        val originalRadius = displayCircle.radius * ((scaleX + scaleY) / 2f) // 平均缩放半径\n        return Circle(originalCenter, originalRadius, displayCircle.shapeProperties)\n    }\n    \n    /**\n     * 转换三角形坐标\n     */\n    fun convertTriangleToOriginal(displayTriangle: Triangle): Triangle {\n        val originalFirst = displayToOriginal(displayTriangle.first)\n        val originalSecond = displayTriangle.second?.let { displayToOriginal(it) }\n        val originalThird = displayTriangle.third?.let { displayToOriginal(it) }\n        return Triangle(originalFirst, originalSecond, originalThird, displayTriangle.shapeProperties)\n    }\n    \n    /**\n     * 转换矩形坐标\n     */\n    fun convertRectangleToOriginal(displayRect: Rectangle): Rectangle {\n        val originalTl = displayToOriginal(displayRect.tl)\n        val originalBl = displayToOriginal(displayRect.bl)\n        val originalBr = displayToOriginal(displayRect.br)\n        val originalTr = displayToOriginal(displayRect.tr)\n        val originalFirst = displayToOriginal(displayRect.rectFirst)\n        return Rectangle(originalTl, originalBl, originalBr, originalTr, originalFirst, displayRect.shapeProperties)\n    }\n    \n    /**\n     * 转换多边形坐标\n     */\n    fun convertPolygonToOriginal(displayPolygon: Polygon): Polygon {\n        val originalPoints = displayPolygon.points.map { displayToOriginal(it) }\n        return Polygon(originalPoints, displayPolygon.shapeProperties)\n    }\n    \n    /**\n     * 转换文字坐标\n     */\n    fun convertTextToOriginal(displayText: Text): Text {\n        val originalPoint = displayToOriginal(displayText.point)\n        return Text(originalPoint, displayText.message, displayText.shapeProperties)\n    }\n}\n"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/shapedrawing/geometry/CanvasDrawer.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.shapedrawing.geometry\n\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.geometry.Rect\nimport androidx.compose.ui.graphics.*\n\n/**\n * 文本绘制器接口\n * 定义了文本绘制的契约\n */\ninterface TextDrawer {\n    /**\n     * 在指定位置绘制文本\n     * \n     * @param canvas 画布\n     * @param pos 位置\n     * @param text 文本列表\n     * @param color 颜色\n     * @param fontSize 字体大小\n     */\n    fun text(canvas: Canvas, pos: Offset, text: List<String>, color: Color, fontSize: Float)\n}\n\n/**\n * 画布绘制器\n * 实现了Drawer接口，提供具体的绘制功能\n * \n * @param textDrawer 文本绘制器\n * @param canvas 画布对象\n * \n * @author Tony Shen\n * @date 2024/11/20 11:07\n * @version V1.0\n */\nclass CanvasDrawer(\n    private val textDrawer: TextDrawer, \n    private val canvas: Canvas\n) : Drawer {\n\n    override fun point(offset: Offset, color: Color) {\n        try {\n            canvas.drawCircle(offset, 4f, Paint().apply { \n                this.color = color \n            })\n        } catch (e: Exception) {\n            // 记录错误但不中断绘制\n            println(\"Error drawing point at $offset: ${e.message}\")\n        }\n    }\n\n    override fun circle(center: Offset, radius: Float, style: Style) {\n        try {\n            style.styled { paint ->\n                canvas.drawCircle(center, radius, paint)\n            }\n        } catch (e: Exception) {\n            println(\"Error drawing circle at $center with radius $radius: ${e.message}\")\n        }\n    }\n\n    override fun line(from: Offset, to: Offset, style: Style) {\n        try {\n            style.styled { paint ->\n                canvas.drawLine(from, to, paint)\n            }\n        } catch (e: Exception) {\n            println(\"Error drawing line from $from to $to: ${e.message}\")\n        }\n    }\n\n    override fun polygon(points: List<Offset>, style: Style) {\n        if (points.size < 2) {\n            println(\"Warning: Polygon must have at least 2 points\")\n            return\n        }\n        \n        try {\n            val path = Path().apply {\n                moveTo(points[0].x, points[0].y)\n                points.drop(1).forEach { point ->\n                    lineTo(point.x, point.y)\n                }\n                close()\n            }\n            \n            style.styled { paint ->\n                canvas.drawPath(path, paint)\n            }\n        } catch (e: Exception) {\n            println(\"Error drawing polygon with ${points.size} points: ${e.message}\")\n        }\n    }\n\n    override fun text(pos: Offset, text: List<String>, color: Color, fontSize: Float) {\n        try {\n            textDrawer.text(canvas, pos, text, color, fontSize)\n        } catch (e: Exception) {\n            println(\"Error drawing text at $pos: ${e.message}\")\n        }\n    }\n\n    override fun angle(center: Offset, from: Float, to: Float, style: Style) {\n        try {\n            val rect = Rect(center, 50f * style.scale)\n            style.styled { paint ->\n                canvas.drawArcRad(rect, -from, from - to, true, paint)\n            }\n        } catch (e: Exception) {\n            println(\"Error drawing angle at $center: ${e.message}\")\n        }\n    }\n    \n    /**\n     * 绘制矩形\n     * \n     * @param rect 矩形区域\n     * @param style 样式\n     */\n    fun rectangle(rect: Rect, style: Style) {\n        try {\n            val path = Path().apply {\n                addRect(rect)\n            }\n            \n            style.styled { paint ->\n                canvas.drawPath(path, paint)\n            }\n        } catch (e: Exception) {\n            println(\"Error drawing rectangle: ${e.message}\")\n        }\n    }\n    \n    /**\n     * 绘制椭圆\n     * \n     * @param rect 椭圆边界矩形\n     * @param style 样式\n     */\n    fun ellipse(rect: Rect, style: Style) {\n        try {\n            style.styled { paint ->\n                canvas.drawOval(rect, paint)\n            }\n        } catch (e: Exception) {\n            println(\"Error drawing ellipse: ${e.message}\")\n        }\n    }\n}\n\n/**\n * Style扩展函数：应用样式到绘制操作\n * \n * @param action 绘制操作，接收Paint参数\n */\nprivate fun Style.styled(action: (Paint) -> Unit) {\n    // 绘制填充\n    if (fill) {\n        action(Paint().apply {\n            color = this@styled.color\n            style = PaintingStyle.Fill\n            alpha = this@styled.alpha\n        })\n    }\n\n    // 绘制边框\n    if (border != Border.No) {\n        action(Paint().apply {\n            color = this@styled.color\n            style = PaintingStyle.Stroke\n            pathEffect = border.effect?.let { PathEffect.dashPathEffect(it) }\n            strokeWidth = 4.2f * scale\n            alpha = this@styled.alpha\n        })\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/shapedrawing/geometry/Drawer.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.shapedrawing.geometry\n\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.geometry.Rect\nimport androidx.compose.ui.graphics.Color\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.shapedrawing.geometry.Drawer\n * @author: Tony Shen\n * @date: 2024/11/20 11:42\n * @version: V1.0 <描述当前版本功能>\n */\ninterface Drawer {\n    val zoom: Float get() = 1f\n    val bounds: Rect get() = Rect(-100f, -100f, 100f, 100f)\n\n    fun point(offset: Offset, color: Color)\n\n    fun circle(center: Offset, radius: Float, style: Style)\n\n    fun line(from: Offset, to: Offset, style: Style)\n\n    fun polygon(points: List<Offset>, style: Style)\n\n    fun text(pos: Offset, text: List<String>, color: Color, fontSize: Float)\n\n    fun angle(center: Offset, from: Float, to: Float, style: Style)\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/shapedrawing/geometry/Style.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.shapedrawing.geometry\n\nimport androidx.compose.ui.graphics.Color\n\n/**\n * 边框样式枚举\n * 定义了不同的边框效果\n * \n * @author Tony Shen\n * @date 2024/11/20 11:45\n * @version V1.0\n */\nenum class Border(val effect: FloatArray?, val description: String) {\n    No(null, \"无边框\"),\n    Dot(floatArrayOf(5f, 5f), \"点状边框\"),\n    Dash(floatArrayOf(25f, 25f), \"虚线边框\"),\n    DashDot(floatArrayOf(25f, 10f, 5f, 10f), \"点划线边框\"),\n    Line(null, \"实线边框\");\n    \n    companion object {\n        /**\n         * 根据描述获取边框样式\n         */\n        fun fromDescription(description: String): Border? = \n            values().find { it.description == description }\n    }\n}\n\n/**\n * 等分组枚举\n * 用于分组相关的样式\n */\nenum class EqualityGroup(val description: String) {\n    Equal1(\"等分组1\"),\n    Equal2(\"等分组2\"),\n    Equal3(\"等分组3\"),\n    EqualV(\"等分组V\"),\n    EqualO(\"等分组O\");\n    \n    companion object {\n        /**\n         * 根据描述获取等分组\n         */\n        fun fromDescription(description: String): EqualityGroup? = \n            values().find { it.description == description }\n    }\n}\n\n/**\n * 文本跨度处理函数\n * 将包含下划线的文本分割为多个部分\n * \n * @param text 输入文本\n * @return 分割后的文本列表\n */\nfun spans(text: String): List<String> = buildList {\n    var last = 0\n    while (true) {\n        var next = text.indexOf('_', last)\n        if (next == text.length - 1 || next == -1) next = text.length\n        add(text.substring(last, next))\n        if (next == text.length) break\n        if (text[next + 1] == '{') {\n            last = text.indexOf('}', next + 1)\n            if (last == 0) error(\"Expected '}'\")\n            add(text.substring(next + 2, last))\n            last++\n        } else {\n            add(text[next + 1].toString())\n            last = next + 2\n        }\n    }\n}\n\n/**\n * 样式数据类\n * 定义了绘制形状时的视觉样式\n * \n * @param name 样式名称列表\n * @param color 颜色\n * @param border 边框样式\n * @param equalityGroup 等分组\n * @param fill 是否填充\n * @param scale 缩放比例\n * @param alpha 透明度\n * @param bounded 是否受边界限制\n */\ndata class Style(\n    val name: List<String>? = null,\n    val color: Color,\n    val border: Border,\n    val equalityGroup: EqualityGroup? = null,\n    val fill: Boolean = false,\n    val scale: Float = 1f,\n    val alpha: Float = 1f,\n    val bounded: Boolean = true\n) {\n    \n    init {\n        require(scale > 0f) { \"Scale must be positive\" }\n        require(alpha in 0f..1f) { \"Alpha must be between 0.0 and 1.0\" }\n    }\n    \n    /**\n     * 检查样式是否有效\n     */\n    fun isValid(): Boolean = \n        scale > 0f && \n        alpha in 0f..1f\n    \n    /**\n     * 创建带有新颜色的副本\n     */\n    fun withColor(newColor: Color): Style = copy(color = newColor)\n    \n    /**\n     * 创建带有新边框的副本\n     */\n    fun withBorder(newBorder: Border): Style = copy(border = newBorder)\n    \n    /**\n     * 创建带有新透明度的副本\n     */\n    fun withAlpha(newAlpha: Float): Style = copy(alpha = newAlpha.coerceIn(0f, 1f))\n    \n    /**\n     * 创建带有新缩放比例的副本\n     */\n    fun withScale(newScale: Float): Style = copy(scale = newScale.coerceAtLeast(0.1f))\n    \n    /**\n     * 创建带有新填充状态的副本\n     */\n    fun withFill(newFill: Boolean): Style = copy(fill = newFill)\n    \n    /**\n     * 创建带有新边界限制的副本\n     */\n    fun withBounded(newBounded: Boolean): Style = copy(bounded = newBounded)\n    \n    companion object {\n        /**\n         * 默认样式\n         */\n        val DEFAULT = Style(\n            color = Color.Black,\n            border = Border.Line,\n            fill = false,\n            scale = 1f,\n            alpha = 1f,\n            bounded = true\n        )\n        \n        /**\n         * 透明样式\n         */\n        val TRANSPARENT = Style(\n            color = Color.Transparent,\n            border = Border.No,\n            fill = false,\n            scale = 1f,\n            alpha = 0f,\n            bounded = true\n        )\n        \n        /**\n         * 填充样式\n         */\n        val FILLED = Style(\n            color = Color.Blue,\n            border = Border.Line,\n            fill = true,\n            scale = 1f,\n            alpha = 1f,\n            bounded = true\n        )\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/shapedrawing/handler/ShapeDrawingEventHandler.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.shapedrawing.handler\n\nimport androidx.compose.ui.geometry.Offset\nimport cn.netdiscovery.monica.ui.controlpanel.shapedrawing.coordinate.CoordinateConverter\nimport cn.netdiscovery.monica.ui.controlpanel.shapedrawing.model.Shape\nimport cn.netdiscovery.monica.ui.controlpanel.shapedrawing.model.Shape.*\nimport cn.netdiscovery.monica.ui.controlpanel.shapedrawing.model.ShapeEnum\nimport cn.netdiscovery.monica.ui.controlpanel.shapedrawing.state.ShapeDrawingState\nimport cn.netdiscovery.monica.ui.controlpanel.shapedrawing.CoordinateSystem\nimport cn.netdiscovery.monica.ui.widget.image.gesture.MotionEvent\nimport cn.netdiscovery.monica.utils.logger\nimport org.slf4j.Logger\n\n/**\n * 形状绘制事件处理器\n * 处理各种形状的绘制逻辑，分离业务逻辑\n * \n * @author Tony Shen\n * @date 2025/9/8\n * @version V1.0\n */\nclass ShapeDrawingEventHandler(\n    private val state: ShapeDrawingState,\n    private val coordinateConverter: CoordinateConverter\n) {\n    private val logger: Logger = logger<ShapeDrawingEventHandler>()\n    \n    /**\n     * 处理鼠标按下事件\n     */\n    fun handleMouseDown(position: Offset) {\n        state.updatePosition(position)\n        state.updateMotionEvent(MotionEvent.Down)\n        \n        when (state.currentShape) {\n            ShapeEnum.Line -> handleLineDown(position)\n            ShapeEnum.Circle -> handleCircleDown(position)\n            ShapeEnum.Triangle -> handleTriangleDown(position)\n            ShapeEnum.Rectangle -> handleRectangleDown(position)\n            ShapeEnum.Polygon -> handlePolygonDown(position)\n            else -> Unit\n        }\n    }\n    \n    /**\n     * 处理鼠标移动事件\n     */\n    fun handleMouseMove(position: Offset): Map<Offset, Shape> {\n        state.updatePosition(position)\n        state.updateMotionEvent(MotionEvent.Move)\n        \n        return when (state.currentShape) {\n            ShapeEnum.Line -> handleLineMove(position)\n            ShapeEnum.Circle -> handleCircleMove(position)\n            ShapeEnum.Triangle -> handleTriangleMove(position)\n            ShapeEnum.Rectangle -> handleRectangleMove(position)\n            ShapeEnum.Polygon -> handlePolygonMove(position)\n            else -> emptyMap()\n        }\n    }\n    \n    /**\n     * 处理鼠标抬起事件\n     */\n    fun handleMouseUp(position: Offset, bitmapWidth: Int, bitmapHeight: Int): Pair<Offset, Shape>? {\n        state.updatePosition(position)\n        state.updateMotionEvent(MotionEvent.Up)\n        \n        return when (state.currentShape) {\n            ShapeEnum.Line -> handleLineUp(bitmapWidth, bitmapHeight)\n            ShapeEnum.Circle -> handleCircleUp(bitmapWidth, bitmapHeight)\n            ShapeEnum.Triangle -> handleTriangleUp(bitmapWidth, bitmapHeight)\n            ShapeEnum.Rectangle -> handleRectangleUp(bitmapWidth, bitmapHeight)\n            ShapeEnum.Polygon -> handlePolygonUp(bitmapWidth, bitmapHeight)\n            else -> null\n        }\n    }\n    \n    // ========== 线段处理 ==========\n    \n    private fun handleLineDown(position: Offset) {\n        if (state.previousPosition != position && state.currentLineStart == Offset.Unspecified) {\n            state.updateLineState(start = position)\n        } else if (state.currentLineStart != Offset.Unspecified) {\n            state.updateLineState(end = position)\n        }\n    }\n    \n    private fun handleLineMove(position: Offset): Map<Offset, Shape> {\n        state.updateLineState(end = position)\n        return if (state.currentLineStart != Offset.Unspecified) {\n            mapOf(state.currentLineStart to Line(state.currentLineStart, position, state.currentShapeProperty))\n        } else emptyMap()\n    }\n    \n    private fun handleLineUp(bitmapWidth: Int, bitmapHeight: Int): Pair<Offset, Shape>? {\n        val startValidation = CoordinateSystem.validateOffset(state.currentLineStart, bitmapWidth, bitmapHeight)\n        val endValidation = CoordinateSystem.validateOffset(state.currentLineEnd, bitmapWidth, bitmapHeight)\n        \n        return if (startValidation.isValid && endValidation.isValid) {\n            val displayLine = Line(state.currentLineStart, state.currentLineEnd, state.currentShapeProperty)\n            val originalLine = coordinateConverter.convertLineToOriginal(displayLine)\n            val lineKey = state.currentLineStart // 保存key，避免在clearCurrentDrawingState后丢失\n            state.addShape(lineKey, displayLine, originalLine)\n            state.recordLastDrawnShape(lineKey, \"Line\")\n            logger.info(\"添加线段: ${state.currentLineStart} -> ${state.currentLineEnd}\")\n            \n            // 重置线段状态，准备下次绘制\n            state.clearCurrentDrawingState()\n            \n            Pair(lineKey, displayLine)\n        } else {\n            logger.warn(\"线段坐标无效: ${startValidation.message}, ${endValidation.message}\")\n            null\n        }\n    }\n    \n    // ========== 圆形处理 ==========\n    \n    private fun handleCircleDown(position: Offset) {\n        if (state.previousPosition != position && state.currentCircleCenter == Offset.Unspecified) {\n            state.updateCircleState(center = position)\n        }\n    }\n    \n    private fun handleCircleMove(position: Offset): Map<Offset, Shape> {\n        val radius = CoordinateSystem.calculateCircleRadius(state.currentCircleCenter, position)\n        state.updateCircleState(radius = radius)\n        return if (state.currentCircleCenter != Offset.Unspecified) {\n            mapOf(state.currentCircleCenter to Circle(state.currentCircleCenter, radius, state.currentShapeProperty))\n        } else emptyMap()\n    }\n    \n    private fun handleCircleUp(bitmapWidth: Int, bitmapHeight: Int): Pair<Offset, Shape>? {\n        val centerValidation = CoordinateSystem.validateOffset(state.currentCircleCenter, bitmapWidth, bitmapHeight)\n        \n        return if (centerValidation.isValid && state.currentCircleRadius > 0) {\n            val displayCircle = Circle(state.currentCircleCenter, state.currentCircleRadius, state.currentShapeProperty)\n            val originalCircle = coordinateConverter.convertCircleToOriginal(displayCircle)\n            val circleKey = state.currentCircleCenter // 保存key，避免在clearCurrentDrawingState后丢失\n            state.addShape(circleKey, displayCircle, originalCircle)\n            state.recordLastDrawnShape(circleKey, \"Circle\")\n            logger.info(\"添加圆形: 中心=${state.currentCircleCenter}, 半径=${state.currentCircleRadius}\")\n            \n            // 重置圆形状态，准备下次绘制\n            state.clearCurrentDrawingState()\n            \n            Pair(circleKey, displayCircle)\n        } else {\n            logger.warn(\"圆形坐标无效: ${centerValidation.message}\")\n            null\n        }\n    }\n    \n    // ========== 三角形处理 ==========\n    \n    private fun handleTriangleDown(position: Offset) {\n        determineTriangleCoordinates()\n    }\n    \n    private fun handleTriangleMove(position: Offset): Map<Offset, Shape> {\n        determineTriangleCoordinates()\n        return if (state.currentTriangleFirst != Offset.Unspecified && \n                   state.currentTriangleSecond != Offset.Unspecified && \n                   state.currentTriangleThird != Offset.Unspecified) {\n            val triangle = Triangle(state.currentTriangleFirst, state.currentTriangleSecond, state.currentTriangleThird, state.currentShapeProperty)\n            mapOf(state.currentTriangleFirst to triangle)\n        } else emptyMap()\n    }\n    \n    private fun handleTriangleUp(bitmapWidth: Int, bitmapHeight: Int): Pair<Offset, Shape>? {\n        val firstValidation = CoordinateSystem.validateOffset(state.currentTriangleFirst, bitmapWidth, bitmapHeight)\n        val secondValidation = CoordinateSystem.validateOffset(state.currentTriangleSecond, bitmapWidth, bitmapHeight)\n        val thirdValidation = CoordinateSystem.validateOffset(state.currentTriangleThird, bitmapWidth, bitmapHeight)\n        \n        return if (firstValidation.isValid && secondValidation.isValid && thirdValidation.isValid) {\n            val displayTriangle = Triangle(state.currentTriangleFirst, state.currentTriangleSecond, state.currentTriangleThird, state.currentShapeProperty)\n            val originalTriangle = coordinateConverter.convertTriangleToOriginal(displayTriangle)\n            val triangleKey = state.currentTriangleFirst // 保存key，避免在clearCurrentDrawingState后丢失\n            state.addShape(triangleKey, displayTriangle, originalTriangle)\n            state.recordLastDrawnShape(triangleKey, \"Triangle\")\n            logger.info(\"添加三角形: ${state.currentTriangleFirst}, ${state.currentTriangleSecond}, ${state.currentTriangleThird}\")\n            \n            // 重置三角形状态，准备下次绘制\n            state.clearCurrentDrawingState()\n            \n            Pair(triangleKey, displayTriangle)\n        } else {\n            logger.warn(\"三角形坐标无效: ${firstValidation.message}, ${secondValidation.message}, ${thirdValidation.message}\")\n            null\n        }\n    }\n    \n    private fun determineTriangleCoordinates() {\n        if (state.previousPosition != state.currentPosition && state.currentTriangleFirst == Offset.Unspecified) {\n            state.updateTriangleState(first = state.currentPosition)\n        } else if (state.currentTriangleFirst != Offset.Unspecified && \n                   state.currentTriangleSecond == Offset.Unspecified && \n                   state.currentTriangleFirst != state.currentPosition) {\n            state.updateTriangleState(second = state.currentPosition)\n        } else if (state.currentTriangleFirst != Offset.Unspecified && \n                   state.currentTriangleSecond != Offset.Unspecified &&\n                   state.currentTriangleThird == Offset.Unspecified &&\n                   state.currentTriangleSecond != state.currentPosition) {\n            state.updateTriangleState(third = state.currentPosition)\n        }\n    }\n    \n    // ========== 矩形处理 ==========\n    \n    private fun handleRectangleDown(position: Offset) {\n        if (state.previousPosition != position && state.currentRectTL == Offset.Unspecified) {\n            state.updateRectangleState(tl = position, first = position)\n        } else if (state.currentRectTL != Offset.Unspecified) {\n            state.updateRectangleState(br = position)\n            determineRectangleCoordinates()\n        }\n    }\n    \n    private fun handleRectangleMove(position: Offset): Map<Offset, Shape> {\n        state.updateRectangleState(br = position)\n        determineRectangleCoordinates()\n        return if (state.currentRectFirst != Offset.Unspecified) {\n            val rect = Rectangle(state.currentRectTL, state.currentRectBL, state.currentRectBR, state.currentRectTR, state.currentRectFirst, state.currentShapeProperty)\n            mapOf(state.currentRectFirst to rect)\n        } else emptyMap()\n    }\n    \n    private fun handleRectangleUp(bitmapWidth: Int, bitmapHeight: Int): Pair<Offset, Shape>? {\n        val tlValidation = CoordinateSystem.validateOffset(state.currentRectTL, bitmapWidth, bitmapHeight)\n        val brValidation = CoordinateSystem.validateOffset(state.currentRectBR, bitmapWidth, bitmapHeight)\n        \n        return if (tlValidation.isValid && brValidation.isValid) {\n            val displayRect = Rectangle(state.currentRectTL, state.currentRectBL, state.currentRectBR, state.currentRectTR, state.currentRectFirst, state.currentShapeProperty)\n            val originalRect = coordinateConverter.convertRectangleToOriginal(displayRect)\n            val rectKey = state.currentRectFirst // 保存key，避免在clearCurrentDrawingState后丢失\n            state.addShape(rectKey, displayRect, originalRect)\n            state.recordLastDrawnShape(rectKey, \"Rectangle\")\n            logger.info(\"添加矩形: ${state.currentRectTL} -> ${state.currentRectBR}\")\n            \n            // 重置矩形状态，准备下次绘制\n            state.clearCurrentDrawingState()\n            \n            Pair(rectKey, displayRect)\n        } else {\n            logger.warn(\"矩形坐标无效: ${tlValidation.message}, ${brValidation.message}\")\n            null\n        }\n    }\n    \n    private fun determineRectangleCoordinates() {\n        if (state.currentRectBR.x > state.currentRectFirst.x && state.currentRectBR.y > state.currentRectFirst.y) {\n            if (state.currentRectTL != state.currentRectFirst) {\n                state.updateRectangleState(tl = state.currentRectFirst)\n            }\n            state.updateRectangleState(\n                tr = Offset(state.currentRectBR.x, state.currentRectTL.y),\n                bl = Offset(state.currentRectTL.x, state.currentRectBR.y)\n            )\n        } else if (state.currentRectBR.x > state.currentRectFirst.x && state.currentRectBR.y < state.currentRectFirst.y) {\n            if (state.currentRectTL != state.currentRectFirst) {\n                state.updateRectangleState(tl = state.currentRectFirst)\n            }\n            state.updateRectangleState(\n                bl = state.currentRectTL,\n                tr = state.currentRectBR,\n                tl = Offset(state.currentRectBL.x, state.currentRectTR.y),\n                br = Offset(state.currentRectTR.x, state.currentRectBL.y)\n            )\n        } else if (state.currentRectBR.x < state.currentRectFirst.x && state.currentRectBR.y > state.currentRectFirst.y) {\n            if (state.currentRectTL != state.currentRectFirst) {\n                state.updateRectangleState(tl = state.currentRectFirst)\n            }\n            state.updateRectangleState(\n                tr = state.currentRectTL,\n                bl = state.currentRectBR,\n                tl = Offset(state.currentRectBL.x, state.currentRectTR.y),\n                br = Offset(state.currentRectTR.x, state.currentRectBL.y)\n            )\n        } else if (state.currentRectBR.x < state.currentRectFirst.x && state.currentRectBR.y < state.currentRectFirst.y) {\n            if (state.currentRectTL != state.currentRectFirst) {\n                state.updateRectangleState(tl = state.currentRectFirst)\n            }\n            val temp = state.currentRectTL\n            state.updateRectangleState(\n                tl = state.currentRectBR,\n                br = temp,\n                tr = Offset(state.currentRectBR.x, state.currentRectTL.y),\n                bl = Offset(state.currentRectTL.x, state.currentRectBR.y)\n            )\n        }\n    }\n    \n    // ========== 多边形处理 ==========\n    \n    private fun handlePolygonDown(position: Offset) {\n        if (state.previousPosition != position && state.currentPolygonFirst == Offset.Unspecified) {\n            state.updatePolygonState(first = position, addPoint = position)\n        } else if (state.currentPolygonFirst != Offset.Unspecified) {\n            state.updatePolygonState(addPoint = position)\n        }\n    }\n    \n    private fun handlePolygonMove(position: Offset): Map<Offset, Shape> {\n        state.updatePolygonState(addPoint = position)\n        return if (state.currentPolygonFirst != Offset.Unspecified) {\n            val polygon = Polygon(state.currentPolygonPoints.toList(), state.currentShapeProperty)\n            mapOf(state.currentPolygonFirst to polygon)\n        } else emptyMap()\n    }\n    \n    private fun handlePolygonUp(bitmapWidth: Int, bitmapHeight: Int): Pair<Offset, Shape>? {\n        return if (state.currentPolygonPoints.size >= 3) {\n            val boundaryValidation = CoordinateSystem.validateShapeBoundary(state.currentPolygonPoints.toList(), bitmapWidth, bitmapHeight)\n            \n            if (boundaryValidation.isValid) {\n                val displayPolygon = Polygon(state.currentPolygonPoints.toList(), state.currentShapeProperty)\n                val originalPolygon = coordinateConverter.convertPolygonToOriginal(displayPolygon)\n                val polygonKey = state.currentPolygonFirst // 保存key，避免在clearCurrentDrawingState后丢失\n                state.addShape(polygonKey, displayPolygon, originalPolygon)\n                state.recordLastDrawnShape(polygonKey, \"Polygon\")\n                logger.info(\"添加多边形: ${state.currentPolygonPoints.size}个顶点\")\n                \n                // 重置多边形状态，准备下次绘制\n                state.clearCurrentDrawingState()\n                \n                Pair(polygonKey, displayPolygon)\n            } else {\n                logger.warn(\"多边形边界无效: ${boundaryValidation.message}\")\n                null\n            }\n        } else {\n            logger.warn(\"多边形顶点数量不足: ${state.currentPolygonPoints.size} < 3\")\n            null\n        }\n    }\n}\n"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/shapedrawing/helper/SpecialLayerHelper.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.shapedrawing.helper\n\nimport androidx.compose.ui.graphics.ImageBitmap\nimport cn.netdiscovery.monica.ui.controlpanel.shapedrawing.layer.ImageLayer\nimport cn.netdiscovery.monica.ui.controlpanel.shapedrawing.layer.Layer\nimport cn.netdiscovery.monica.ui.controlpanel.shapedrawing.layer.LayerManager\nimport cn.netdiscovery.monica.ui.controlpanel.shapedrawing.layer.LayerType\nimport java.util.UUID\n\n/**\n * 背景层助手类，集中管理特殊图层（如背景层）的逻辑\n * 包括缓存管理、查询、创建和更新\n */\nclass SpecialLayerHelper(\n    private val layerManager: LayerManager,\n    private val backgroundLayerName: String = BACKGROUND_LAYER_NAME\n) {\n    private var cachedBackgroundLayer: ImageLayer? = null\n    private var cachedBackgroundLayerId: UUID? = null\n\n    companion object {\n        const val BACKGROUND_LAYER_NAME = \"背景图层\"\n    }\n\n    /**\n     * 获取背景层，带缓存机制\n     */\n    fun getBackgroundLayer(): ImageLayer? {\n        // 验证缓存是否仍然有效\n        validateCache()\n        if (cachedBackgroundLayer != null) {\n            return cachedBackgroundLayer\n        }\n\n        // 缓存失效或不存在，重新查找\n        val found = findBackgroundLayer()\n        cachedBackgroundLayer = found\n        cachedBackgroundLayerId = found?.id\n        return found\n    }\n\n    /**\n     * 获取或创建背景层\n     */\n    fun getOrCreateBackgroundLayer(image: ImageBitmap): ImageLayer {\n        val existing = getBackgroundLayer()\n        if (existing != null) {\n            return existing\n        }\n\n        // 创建新的背景层\n        val newLayer = ImageLayer(backgroundLayerName, image)\n        layerManager.addLayer(newLayer, index = 0)\n        cachedBackgroundLayer = newLayer\n        cachedBackgroundLayerId = newLayer.id\n        return newLayer\n    }\n\n    /**\n     * 更新背景层图像\n     */\n    fun updateBackgroundLayer(image: ImageBitmap) {\n        val layer = getOrCreateBackgroundLayer(image)\n        layer.updateImage(image)\n    }\n\n    /**\n     * 检查是否存在背景层\n     */\n    fun hasBackgroundLayer(): Boolean {\n        return getBackgroundLayer() != null\n    }\n\n    /**\n     * 移除背景层\n     */\n    fun removeBackgroundLayer(): Boolean {\n        val bgLayer = getBackgroundLayer() ?: return false\n        invalidateCache()\n        return layerManager.removeLayer(bgLayer.id) != null\n    }\n\n    /**\n     * 检查是否为背景层\n     */\n    fun isBackgroundLayer(layer: Layer): Boolean {\n        return layer.name == backgroundLayerName && layer.type == LayerType.IMAGE\n    }\n\n    /**\n     * 验证缓存的有效性\n     */\n    private fun validateCache() {\n        if (cachedBackgroundLayer == null || cachedBackgroundLayerId == null) {\n            return\n        }\n\n        // 检查缓存的图层是否仍在图层管理器中\n        val stillExists = layerManager.getLayerById(cachedBackgroundLayerId!!) != null\n        if (!stillExists) {\n            invalidateCache()\n        }\n    }\n\n    /**\n     * 清空缓存\n     */\n    private fun invalidateCache() {\n        cachedBackgroundLayer = null\n        cachedBackgroundLayerId = null\n    }\n\n    /**\n     * 在图层管理器中查找背景层\n     */\n    private fun findBackgroundLayer(): ImageLayer? {\n        return layerManager.layers.value\n            .firstOrNull { isBackgroundLayer(it) } as? ImageLayer\n    }\n}\n\n"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/shapedrawing/layer/ImageLayer.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.shapedrawing.layer\n\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.geometry.Rect\nimport androidx.compose.ui.graphics.ImageBitmap\nimport androidx.compose.ui.graphics.drawscope.DrawScope\nimport androidx.compose.ui.graphics.drawscope.clipPath\nimport androidx.compose.ui.graphics.Path\nimport androidx.compose.ui.graphics.drawscope.withTransform\nimport androidx.compose.ui.unit.IntSize\nimport cn.netdiscovery.monica.ui.controlpanel.shapedrawing.EditorController\n\n/**\n * 图像图层，负责持有位图及其变换信息。\n */\nclass ImageLayer(\n    name: String,\n    image: ImageBitmap? = null,\n    transform: LayerTransform = LayerTransform()\n) : Layer(\n    type = LayerType.IMAGE,\n    name = name\n) {\n\n    var image by mutableStateOf(image)\n        private set\n\n    var transform by mutableStateOf(transform)\n        private set\n\n    fun updateImage(newImage: ImageBitmap?) {\n        if (image != newImage) {\n            image = newImage\n            markDirty()\n        }\n    }\n\n    fun updateTransform(newTransform: LayerTransform) {\n        if (transform != newTransform) {\n            transform = newTransform\n            markDirty()\n        }\n    }\n\n    override fun render(drawScope: DrawScope) {\n        render(drawScope, backgroundSize = null)\n    }\n    \n    /**\n     * 渲染图像层\n     * @param drawScope 绘制作用域\n     * @param backgroundSize 背景图的实际尺寸（宽、高），如果提供则基于背景图尺寸计算 fitScale，否则基于画布尺寸\n     */\n    fun render(drawScope: DrawScope, backgroundSize: Pair<Float, Float>?) {\n        val bitmap = image ?: return\n\n        // 获取画布尺寸\n        val canvasWidth = drawScope.size.width\n        val canvasHeight = drawScope.size.height\n        \n        // 安全检查：防止除零错误\n        if (bitmap.width <= 0 || bitmap.height <= 0 || canvasWidth <= 0 || canvasHeight <= 0) {\n            return\n        }\n        \n        // 判断是否为背景图层（使用常量，避免硬编码）\n        val isBackgroundLayer = name == EditorController.BACKGROUND_LAYER_NAME\n        \n        if (isBackgroundLayer) {\n            // 背景图层：填充整个画布绘制区域，不保持宽高比（与涂鸦模块一致）\n            drawScope.drawImage(\n                bitmap,\n                dstSize = IntSize(canvasWidth.toInt(), canvasHeight.toInt()),\n                alpha = opacity\n            )\n        } else {\n            // 用户添加的图像层：适应背景图或画布并居中显示\n            // 计算图像缩放比例，保持宽高比，适应背景图或画布（不放大，只缩小）\n            // 如果提供了背景层尺寸，需要考虑背景层在画布上的显示比例\n            val fitScale = if (backgroundSize != null) {\n                val referenceWidth = backgroundSize.first\n                val referenceHeight = backgroundSize.second\n                \n                // 计算背景层在画布上的缩放比例（背景层被拉伸到画布尺寸）\n                val backgroundScaleX = canvasWidth / referenceWidth\n                val backgroundScaleY = canvasHeight / referenceHeight\n                \n                // 计算基于背景层原始尺寸的缩放比例，确保图像尺寸不超过背景层尺寸\n                val referenceScaleX = referenceWidth / bitmap.width\n                val referenceScaleY = referenceHeight / bitmap.height\n                val referenceFitScale = minOf(referenceScaleX, referenceScaleY).coerceAtMost(1f)\n                \n                // 图像层在背景层原始坐标系中的缩放比例\n                // 然后需要乘以背景层在画布上的缩放比例，得到在画布坐标系中的缩放比例\n                referenceFitScale * minOf(backgroundScaleX, backgroundScaleY)\n            } else {\n                // 没有背景层时，基于画布尺寸\n                val canvasScaleX = canvasWidth / bitmap.width\n                val canvasScaleY = canvasHeight / bitmap.height\n                minOf(canvasScaleX, canvasScaleY).coerceAtMost(1f)\n            }\n            \n            // 计算缩放后的图像尺寸\n            val scaledWidth = bitmap.width * fitScale\n            val scaledHeight = bitmap.height * fitScale\n            \n            // 计算居中位置\n            val centerOffsetX = (canvasWidth - scaledWidth) / 2f\n            val centerOffsetY = (canvasHeight - scaledHeight) / 2f\n            \n            // 适应后图像的中心点（在画布坐标系中）\n            val adaptedImageCenter = Offset(\n                centerOffsetX + scaledWidth / 2f,\n                centerOffsetY + scaledHeight / 2f\n            )\n            \n            // 计算图像中心点（在图像坐标系中）\n            val imageCenter = Offset(bitmap.width / 2f, bitmap.height / 2f)\n            \n            // 计算 pivot（在图像坐标系中）\n            val pivot = if (transform.pivot == Offset.Zero) {\n                imageCenter\n            } else {\n                imageCenter + transform.pivot\n            }\n            \n            // translation 存储的是相对于图像中心的偏移（在图像原始坐标系中）\n            // 在 withTransform 中，后写的变换先执行，所以实际执行顺序是：\n            // 1. 用户缩放（相对于 pivot，在图像原始坐标系中）\n            // 2. 用户旋转（相对于 pivot，在图像原始坐标系中）\n            // 3. 用户平移（translation，在图像原始坐标系中）\n            // 4. 自动缩放（fitScale）- 将图像坐标系转换到适应后的坐标系\n            // 5. 自动平移（centerOffset）- 在画布坐标系中\n            val translation = transform.translation\n\n            drawScope.withTransform({\n                // 变换顺序（从外到内，withTransform 的执行顺序）：\n                // 注意：withTransform 中后写的变换先执行，所以应该先写自动适应，再写用户变换\n                \n                // 1. 自动平移（centerOffset）- 最外层，最后执行\n                translate(centerOffsetX, centerOffsetY)\n                \n                // 2. 自动缩放（fitScale）- 将图像坐标系转换到适应后的坐标系\n                scale(fitScale, fitScale)\n                \n                // 3. 用户平移（在图像原始坐标系中，相对于图像中心）\n                // 由于 translation 是相对于图像中心的偏移，需要先平移到图像中心，应用偏移，再平移回去\n                if (translation != Offset.Zero) {\n                    translate(imageCenter.x, imageCenter.y)\n                    translate(translation.x, translation.y)\n                    translate(-imageCenter.x, -imageCenter.y)\n                }\n                \n                // 4. 用户旋转（相对于 pivot，在图像原始坐标系中）\n                if (transform.rotation != 0f) {\n                    translate(pivot.x, pivot.y)\n                    rotate(transform.rotation)\n                    translate(-pivot.x, -pivot.y)\n                }\n                \n                // 5. 用户缩放（相对于 pivot，在图像原始坐标系中）- 最内层，最先执行\n                if (transform.scaleX != 1f || transform.scaleY != 1f) {\n                    translate(pivot.x, pivot.y)\n                    scale(transform.scaleX, transform.scaleY)\n                    translate(-pivot.x, -pivot.y)\n                }\n            }) {\n                // 应用裁剪区域（如果存在）\n                val cropRect = transform.cropRect\n                if (cropRect != null) {\n                    // 裁剪区域是在图像坐标系中定义的\n                    // 由于 withTransform 已经应用了 fitScale，裁剪区域也在当前坐标系中\n                    // 使用 clipPath 来裁剪\n                    val clipPath = Path().apply {\n                        addRect(cropRect)\n                    }\n                    drawScope.clipPath(clipPath) {\n                        drawScope.drawImage(bitmap, alpha = opacity)\n                    }\n                } else {\n                    drawScope.drawImage(bitmap, alpha = opacity)\n                }\n            }\n        }\n    }\n}\n\n/**\n * 图层变换信息。\n * \n * @param translation 相对于图像中心的偏移（在图像原始坐标系中）\n * @param scaleX X轴缩放比例\n * @param scaleY Y轴缩放比例\n * @param rotation 旋转角度（度）\n * @param pivot 旋转和缩放的枢轴点（在图像坐标系中，相对于图像中心）\n * @param cropRect 裁剪区域（在图像坐标系中，null表示不裁剪）\n */\ndata class LayerTransform(\n    val translation: Offset = Offset.Zero,\n    val scaleX: Float = 1f,\n    val scaleY: Float = 1f,\n    val rotation: Float = 0f,\n    val pivot: Offset = Offset.Zero,\n    val cropRect: Rect? = null\n)\n\n"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/shapedrawing/layer/Layer.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.shapedrawing.layer\n\nimport androidx.compose.runtime.Stable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.graphics.drawscope.DrawScope\nimport java.util.UUID\n\n/**\n * 图层类型枚举\n *\n * 目前仅支持图像层与形状层，未来可扩展到调整层、文本层等。\n */\nenum class LayerType {\n    IMAGE,\n    SHAPE\n}\n\n/**\n * Layer 抽象基类，封装所有图层的公共属性与生命周期。\n *\n * @property type 图层类型\n * @property id 图层唯一标识\n * @property name 图层名称\n * @property visible 是否可见\n * @property opacity 透明度，范围 0f..1f\n * @property locked 是否锁定（锁定后禁止编辑/变换）\n */\n@Stable\nabstract class Layer(\n    val type: LayerType,\n    val id: UUID = UUID.randomUUID(),\n    name: String,\n    visible: Boolean = true,\n    opacity: Float = 1f,\n    locked: Boolean = false\n) {\n\n    var name by mutableStateOf(name)\n        private set\n\n    var visible by mutableStateOf(visible)\n        private set\n\n    var opacity by mutableStateOf(opacity.coerceIn(0f, 1f))\n        private set\n\n    var locked by mutableStateOf(locked)\n        private set\n\n    /**\n     * 图层版本号，用于标记图层变化，优化渲染缓存\n     * 当图层属性变化时，版本号递增，触发缓存失效\n     * \n     * 注意：使用普通变量而非 State，避免在 Compose 中读取时触发不必要的重组\n     * 版本号主要用于标记变化，不直接参与 Compose 状态管理\n     */\n    private var _version = 0L\n    val version: Long get() = _version\n\n    /**\n     * 标记图层为脏状态，递增版本号\n     * 子类在属性变化时应调用此方法\n     */\n    protected fun markDirty() {\n        _version++\n    }\n\n    /**\n     * 重命名当前图层。\n     */\n    fun rename(newName: String) {\n        if (name != newName) {\n            name = newName\n            markDirty()\n        }\n    }\n\n    /**\n     * 切换图层可见性。\n     */\n    fun setVisibility(isVisible: Boolean) {\n        if (visible != isVisible) {\n            visible = isVisible\n            markDirty()\n        }\n    }\n\n    /**\n     * 更新透明度，范围自动限制在 0f..1f。\n     */\n    fun updateOpacity(alpha: Float) {\n        val newOpacity = alpha.coerceIn(0f, 1f)\n        if (opacity != newOpacity) {\n            opacity = newOpacity\n            markDirty()\n        }\n    }\n\n    /**\n     * 切换锁定状态。\n     */\n    fun updateLocked(isLocked: Boolean) {\n        if (locked != isLocked) {\n            locked = isLocked\n            markDirty()\n        }\n    }\n\n    /**\n     * 当图层被加入 LayerManager 时回调。\n     */\n    open fun onAttach() {}\n\n    /**\n     * 当图层被移出 LayerManager 时回调。\n     */\n    open fun onDetach() {}\n\n    /**\n     * 将当前图层绘制到指定的 [DrawScope] 中。\n     *\n     * 默认实现为空，由具体图层自行实现。\n     */\n    open fun render(drawScope: DrawScope) = Unit\n}\n\n"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/shapedrawing/layer/LayerManager.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.shapedrawing.layer\n\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport java.util.UUID\nimport java.util.concurrent.CopyOnWriteArraySet\nimport java.util.concurrent.locks.ReentrantLock\nimport kotlin.concurrent.withLock\n\n/**\n * 负责管理图层列表的核心类，提供增删改查、排序以及事件监听能力。\n */\nclass LayerManager {\n\n    private val lock = ReentrantLock()\n\n    private val _layers = MutableStateFlow<List<Layer>>(emptyList())\n    val layers: StateFlow<List<Layer>> = _layers.asStateFlow()\n\n    private val _activeLayer = MutableStateFlow<Layer?>(null)\n    val activeLayer: StateFlow<Layer?> = _activeLayer.asStateFlow()\n\n    private val layerObservers = CopyOnWriteArraySet<LayerListObserver>()\n    private val activeLayerObservers = CopyOnWriteArraySet<ActiveLayerObserver>()\n\n    /**\n     * 添加图层。默认追加到末尾，可通过 [index] 指定插入位置。\n     */\n    fun addLayer(layer: Layer, index: Int? = null) {\n        lock.withLock {\n            val mutable = _layers.value.toMutableList()\n            val insertIndex = index?.coerceIn(0, mutable.size) ?: mutable.size\n            mutable.add(insertIndex, layer)\n            _layers.value = mutable.toList()\n            _activeLayer.value = layer\n        }\n        layer.onAttach()\n        notifyLayerObservers()\n        notifyActiveLayerObservers()\n    }\n\n    /**\n     * 根据 [layerId] 移除图层。\n     * @return 被移除的图层实例，若未找到则返回 null。\n     */\n    fun removeLayer(layerId: UUID): Layer? {\n        var removed: Layer? = null\n        lock.withLock {\n            val mutable = _layers.value.toMutableList()\n            val index = mutable.indexOfFirst { it.id == layerId }\n            if (index != -1) {\n                removed = mutable.removeAt(index)\n                _layers.value = mutable.toList()\n                if (_activeLayer.value?.id == layerId) {\n                    _activeLayer.value = mutable.lastOrNull()\n                }\n            }\n        }\n        removed?.onDetach()\n        if (removed != null) {\n            notifyLayerObservers()\n            notifyActiveLayerObservers()\n        }\n        return removed\n    }\n\n    /**\n     * 清空所有图层。\n     */\n    fun clear() {\n        val detached: List<Layer>\n        lock.withLock {\n            detached = _layers.value\n            _layers.value = emptyList()\n            _activeLayer.value = null\n        }\n        detached.forEach { it.onDetach() }\n        notifyLayerObservers()\n        notifyActiveLayerObservers()\n    }\n\n    /**\n     * 将图层上移一层。\n     */\n    fun moveLayerUp(layerId: UUID): Boolean = moveByOffset(layerId, 1)\n\n    /**\n     * 将图层下移一层。\n     */\n    fun moveLayerDown(layerId: UUID): Boolean = moveByOffset(layerId, -1)\n\n    /**\n     * 将图层移动到指定位置。\n     */\n    fun moveLayerTo(layerId: UUID, index: Int): Boolean {\n        var moved = false\n        lock.withLock {\n            val mutable = _layers.value.toMutableList()\n            val currentIndex = mutable.indexOfFirst { it.id == layerId }\n            if (currentIndex != -1) {\n                val targetIndex = index.coerceIn(0, mutable.lastIndex.coerceAtLeast(0))\n                if (currentIndex != targetIndex) {\n                    val layer = mutable.removeAt(currentIndex)\n                    mutable.add(targetIndex, layer)\n                    _layers.value = mutable.toList()\n                    moved = true\n                }\n            }\n        }\n        if (moved) notifyLayerObservers()\n        return moved\n    }\n\n    /**\n     * 设置当前激活的图层。\n     */\n    fun setActiveLayer(layerId: UUID?) {\n        var shouldNotify = false\n        lock.withLock {\n            val target = layerId?.let { id ->\n                _layers.value.firstOrNull { it.id == id }\n            }\n            if (_activeLayer.value !== target) {\n                _activeLayer.value = target\n                shouldNotify = true\n            }\n        }\n        if (shouldNotify) notifyActiveLayerObservers()\n    }\n\n    /**\n     * 重命名图层。\n     */\n    fun renameLayer(layerId: UUID, newName: String): Boolean =\n        updateLayer(layerId, { it.name != newName }) { it.rename(newName) }\n\n    /**\n     * 更新图层可见性。\n     */\n    fun setLayerVisibility(layerId: UUID, visible: Boolean): Boolean =\n        updateLayer(layerId, { it.visible != visible }) { it.setVisibility(visible) }\n\n    /**\n     * 更新图层透明度。\n     */\n    fun setLayerOpacity(layerId: UUID, opacity: Float): Boolean {\n        val targetOpacity = opacity.coerceIn(0f, 1f)\n        return updateLayer(layerId, { it.opacity != targetOpacity }) { it.updateOpacity(targetOpacity) }\n    }\n\n    /**\n     * 更新图层锁定状态。\n     */\n    fun setLayerLocked(layerId: UUID, locked: Boolean): Boolean =\n        updateLayer(layerId, { it.locked != locked }) { it.updateLocked(locked) }\n\n    /**\n     * 根据 ID 获取图层。\n     */\n    fun getLayerById(layerId: UUID): Layer? = lock.withLock {\n        _layers.value.firstOrNull { it.id == layerId }\n    }\n\n    /**\n     * 批量替换当前图层列表，并指定激活层。\n     */\n    fun replaceLayers(layers: List<Layer>, activeLayerId: UUID? = null) {\n        val previous = lock.withLock {\n            val snapshot = _layers.value\n            _layers.value = layers.toList()\n            _activeLayer.value = activeLayerId?.let { id ->\n                layers.firstOrNull { it.id == id } ?: layers.lastOrNull()\n            }\n            snapshot\n        }\n        previous.forEach { it.onDetach() }\n        layers.forEach { it.onAttach() }\n        notifyLayerObservers()\n        notifyActiveLayerObservers()\n    }\n\n    /**\n     * 注册图层列表监听器，返回用于移除监听的函数。\n     */\n    fun addLayerObserver(observer: LayerListObserver, notifyImmediately: Boolean = true): () -> Unit {\n        layerObservers.add(observer)\n        if (notifyImmediately) {\n            observer.onLayersChanged(_layers.value)\n        }\n        return { layerObservers.remove(observer) }\n    }\n\n    /**\n     * 注册激活图层监听器，返回用于移除监听的函数。\n     */\n    fun addActiveLayerObserver(observer: ActiveLayerObserver, notifyImmediately: Boolean = true): () -> Unit {\n        activeLayerObservers.add(observer)\n        if (notifyImmediately) {\n            observer.onActiveLayerChanged(_activeLayer.value)\n        }\n        return { activeLayerObservers.remove(observer) }\n    }\n\n    /**\n     * 通用的图层属性更新方法，提取了重复的模式\n     * @param layerId 图层ID\n     * @param shouldUpdate 判断是否需要更新的谓词\n     * @param update 执行更新的操作\n     */\n    private fun updateLayer(\n        layerId: UUID,\n        shouldUpdate: (Layer) -> Boolean,\n        update: (Layer) -> Unit\n    ): Boolean {\n        var updated = false\n        lock.withLock {\n            val layer = _layers.value.firstOrNull { it.id == layerId }\n            if (layer != null && shouldUpdate(layer)) {\n                update(layer)\n                updated = true\n            }\n        }\n        if (updated) notifyLayerObservers()\n        return updated\n    }\n\n    private fun moveByOffset(layerId: UUID, offset: Int): Boolean {\n        var moved = false\n        lock.withLock {\n            if (offset == 0) return false\n            val mutable = _layers.value.toMutableList()\n            val currentIndex = mutable.indexOfFirst { it.id == layerId }\n            if (currentIndex != -1 && mutable.isNotEmpty()) {\n                val targetIndex = (currentIndex + offset).coerceIn(0, mutable.lastIndex)\n                if (currentIndex != targetIndex) {\n                    val layer = mutable.removeAt(currentIndex)\n                    mutable.add(targetIndex, layer)\n                    _layers.value = mutable.toList()\n                    moved = true\n                }\n            }\n        }\n        if (moved) notifyLayerObservers()\n        return moved\n    }\n\n    private fun notifyLayerObservers() {\n        if (layerObservers.isEmpty()) return\n        val snapshot = _layers.value\n        layerObservers.forEach { it.onLayersChanged(snapshot) }\n    }\n\n    private fun notifyActiveLayerObservers() {\n        if (activeLayerObservers.isEmpty()) return\n        val active = _activeLayer.value\n        activeLayerObservers.forEach { it.onActiveLayerChanged(active) }\n    }\n}\n\nfun interface LayerListObserver {\n    fun onLayersChanged(layers: List<Layer>)\n}\n\nfun interface ActiveLayerObserver {\n    fun onActiveLayerChanged(activeLayer: Layer?)\n}\n"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/shapedrawing/layer/LayerRenderer.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.shapedrawing.layer\n\nimport androidx.compose.ui.graphics.Paint\nimport androidx.compose.ui.graphics.drawscope.DrawScope\nimport androidx.compose.ui.graphics.drawscope.drawIntoCanvas\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.geometry.Rect\nimport cn.netdiscovery.monica.ui.controlpanel.shapedrawing.ShapeDrawingViewModel\nimport cn.netdiscovery.monica.ui.controlpanel.shapedrawing.geometry.CanvasDrawer\nimport cn.netdiscovery.monica.ui.controlpanel.shapedrawing.widget.TextDrawer\nimport java.util.UUID\n\n/**\n * 负责任务图层绘制与合成的渲染器。\n * \n * 使用图层版本号优化渲染，只有版本变化的图层才会重新渲染。\n */\nclass LayerRenderer(\n    private val layerManager: LayerManager\n) {\n\n    private val opacityPaint = Paint().apply {\n        isAntiAlias = true\n    }\n\n    private val shapeRenderer by lazy { ShapeDrawingViewModel() }\n\n    /**\n     * 将当前 LayerManager 中的图层全部绘制到给定的 [DrawScope]。\n     */\n    fun drawAll(drawScope: DrawScope) {\n        drawAll(drawScope, layerManager.layers.value)\n    }\n\n    /**\n     * 将指定的图层列表绘制到给定的 [DrawScope]。\n     * \n     * 注意：由于 DrawScope 的限制，无法在此层面实现真正的缓存优化。\n     * 图层的 version 字段已准备好，可用于将来在 Compose 层面通过 \n     * remember、key() 和 Modifier.drawWithCache 实现真正的缓存优化。\n     */\n    fun drawAll(drawScope: DrawScope, layers: List<Layer>) {\n        if (layers.isEmpty()) return\n        layers.forEach { layer ->\n            if (!layer.visible || layer.opacity <= 0f) return@forEach\n            drawLayer(drawScope, layer)\n        }\n    }\n\n    /**\n     * 绘制单个图层\n     * \n     * 注意：此方法应该是 internal 或 public，以便在 Compose 层面使用\n     */\n    fun drawLayer(drawScope: DrawScope, layer: Layer) {\n        drawScope.drawIntoCanvas { canvas ->\n            val bounds = Rect(Offset.Zero, drawScope.size)\n            opacityPaint.alpha = layer.opacity.coerceIn(0f, 1f)\n            canvas.saveLayer(bounds, opacityPaint)\n            try {\n                when (layer) {\n                    is ImageLayer -> {\n                        // 获取背景图尺寸（如果存在）\n                        val backgroundLayer = layerManager.layers.value\n                            .firstOrNull { \n                                it.name == cn.netdiscovery.monica.ui.controlpanel.shapedrawing.EditorController.BACKGROUND_LAYER_NAME \n                                && it is ImageLayer \n                            } as? ImageLayer\n                        \n                        val backgroundSize = backgroundLayer?.image?.let { \n                            Pair(it.width.toFloat(), it.height.toFloat()) \n                        }\n                        \n                        layer.render(drawScope, backgroundSize)\n                    }\n                    is ShapeLayer -> drawShapeLayer(drawScope, layer)\n                    else -> layer.render(drawScope)\n                }\n            } finally {\n                canvas.restore()\n            }\n        }\n    }\n\n    private fun drawShapeLayer(drawScope: DrawScope, shapeLayer: ShapeLayer) {\n        if (shapeLayer.isEmpty()) return\n        val canvasDrawer = CanvasDrawer(TextDrawer, drawScope.drawContext.canvas)\n        shapeRenderer.drawShape(\n            canvasDrawer = canvasDrawer,\n            lines = shapeLayer.displayLines,\n            circles = shapeLayer.displayCircles,\n            triangles = shapeLayer.displayTriangles,\n            rectangles = shapeLayer.displayRectangles,\n            polygons = shapeLayer.displayPolygons,\n            texts = shapeLayer.displayTexts,\n            saveFlag = false\n        )\n    }\n}\n\n"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/shapedrawing/layer/ShapeLayer.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.shapedrawing.layer\n\nimport androidx.compose.runtime.mutableStateMapOf\nimport androidx.compose.runtime.snapshots.SnapshotStateMap\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.graphics.drawscope.DrawScope\nimport cn.netdiscovery.monica.ui.controlpanel.shapedrawing.model.Shape\n\n/**\n * 形状图层，负责维护各种几何形状的数据集合。\n *\n * 渲染逻辑会在后续的 LayerRenderer 中统一处理，此处只负责数据管理。\n */\nclass ShapeLayer(\n    name: String\n) : Layer(\n    type = LayerType.SHAPE,\n    name = name\n) {\n\n    val displayLines: SnapshotStateMap<Offset, Shape.Line> = mutableStateMapOf()\n    val originalLines: SnapshotStateMap<Offset, Shape.Line> = mutableStateMapOf()\n\n    val displayCircles: SnapshotStateMap<Offset, Shape.Circle> = mutableStateMapOf()\n    val originalCircles: SnapshotStateMap<Offset, Shape.Circle> = mutableStateMapOf()\n\n    val displayTriangles: SnapshotStateMap<Offset, Shape.Triangle> = mutableStateMapOf()\n    val originalTriangles: SnapshotStateMap<Offset, Shape.Triangle> = mutableStateMapOf()\n\n    val displayRectangles: SnapshotStateMap<Offset, Shape.Rectangle> = mutableStateMapOf()\n    val originalRectangles: SnapshotStateMap<Offset, Shape.Rectangle> = mutableStateMapOf()\n\n    val displayPolygons: SnapshotStateMap<Offset, Shape.Polygon> = mutableStateMapOf()\n    val originalPolygons: SnapshotStateMap<Offset, Shape.Polygon> = mutableStateMapOf()\n\n    val displayTexts: SnapshotStateMap<Offset, Shape.Text> = mutableStateMapOf()\n    val originalTexts: SnapshotStateMap<Offset, Shape.Text> = mutableStateMapOf()\n\n    override fun render(drawScope: DrawScope) = Unit\n\n    fun addShape(key: Offset, displayShape: Shape, originalShape: Shape) {\n        when (displayShape) {\n            is Shape.Line -> {\n                displayLines[key] = displayShape\n                originalLines[key] = (originalShape as? Shape.Line) ?: displayShape\n            }\n\n            is Shape.Circle -> {\n                displayCircles[key] = displayShape\n                originalCircles[key] = (originalShape as? Shape.Circle) ?: displayShape\n            }\n\n            is Shape.Triangle -> {\n                displayTriangles[key] = displayShape\n                originalTriangles[key] = (originalShape as? Shape.Triangle) ?: displayShape\n            }\n\n            is Shape.Rectangle -> {\n                displayRectangles[key] = displayShape\n                originalRectangles[key] = (originalShape as? Shape.Rectangle) ?: displayShape\n            }\n\n            is Shape.Polygon -> {\n                displayPolygons[key] = displayShape\n                originalPolygons[key] = (originalShape as? Shape.Polygon) ?: displayShape\n            }\n\n            is Shape.Text -> {\n                displayTexts[key] = displayShape\n                originalTexts[key] = (originalShape as? Shape.Text) ?: displayShape\n            }\n        }\n    }\n\n    fun removeShape(key: Offset) {\n        displayLines.remove(key)\n        originalLines.remove(key)\n\n        displayCircles.remove(key)\n        originalCircles.remove(key)\n\n        displayTriangles.remove(key)\n        originalTriangles.remove(key)\n\n        displayRectangles.remove(key)\n        originalRectangles.remove(key)\n\n        displayPolygons.remove(key)\n        originalPolygons.remove(key)\n\n        displayTexts.remove(key)\n        originalTexts.remove(key)\n    }\n\n    fun clearShapes() {\n        displayLines.clear()\n        originalLines.clear()\n\n        displayCircles.clear()\n        originalCircles.clear()\n\n        displayTriangles.clear()\n        originalTriangles.clear()\n\n        displayRectangles.clear()\n        originalRectangles.clear()\n\n        displayPolygons.clear()\n        originalPolygons.clear()\n\n        displayTexts.clear()\n        originalTexts.clear()\n    }\n\n    fun isEmpty(): Boolean =\n        displayLines.isEmpty() &&\n            displayCircles.isEmpty() &&\n            displayTriangles.isEmpty() &&\n            displayRectangles.isEmpty() &&\n            displayPolygons.isEmpty() &&\n            displayTexts.isEmpty()\n\n    fun snapshot(): ShapeLayerSnapshot = ShapeLayerSnapshot(\n        displayLines = displayLines.toMap(),\n        originalLines = originalLines.toMap(),\n        displayCircles = displayCircles.toMap(),\n        originalCircles = originalCircles.toMap(),\n        displayTriangles = displayTriangles.toMap(),\n        originalTriangles = originalTriangles.toMap(),\n        displayRectangles = displayRectangles.toMap(),\n        originalRectangles = originalRectangles.toMap(),\n        displayPolygons = displayPolygons.toMap(),\n        originalPolygons = originalPolygons.toMap(),\n        displayTexts = displayTexts.toMap(),\n        originalTexts = originalTexts.toMap()\n    )\n\n    fun restore(snapshot: ShapeLayerSnapshot) {\n        replaceAll(\n            snapshot.displayLines,\n            snapshot.originalLines,\n            snapshot.displayCircles,\n            snapshot.originalCircles,\n            snapshot.displayTriangles,\n            snapshot.originalTriangles,\n            snapshot.displayRectangles,\n            snapshot.originalRectangles,\n            snapshot.displayPolygons,\n            snapshot.originalPolygons,\n            snapshot.displayTexts,\n            snapshot.originalTexts\n        )\n    }\n\n    fun replaceAll(\n        displayLines: Map<Offset, Shape.Line>,\n        originalLines: Map<Offset, Shape.Line>,\n        displayCircles: Map<Offset, Shape.Circle>,\n        originalCircles: Map<Offset, Shape.Circle>,\n        displayTriangles: Map<Offset, Shape.Triangle>,\n        originalTriangles: Map<Offset, Shape.Triangle>,\n        displayRectangles: Map<Offset, Shape.Rectangle>,\n        originalRectangles: Map<Offset, Shape.Rectangle>,\n        displayPolygons: Map<Offset, Shape.Polygon>,\n        originalPolygons: Map<Offset, Shape.Polygon>,\n        displayTexts: Map<Offset, Shape.Text>,\n        originalTexts: Map<Offset, Shape.Text>\n    ) {\n        this.displayLines.clear()\n        this.displayLines.putAll(displayLines)\n        this.originalLines.clear()\n        this.originalLines.putAll(originalLines)\n\n        this.displayCircles.clear()\n        this.displayCircles.putAll(displayCircles)\n        this.originalCircles.clear()\n        this.originalCircles.putAll(originalCircles)\n\n        this.displayTriangles.clear()\n        this.displayTriangles.putAll(displayTriangles)\n        this.originalTriangles.clear()\n        this.originalTriangles.putAll(originalTriangles)\n\n        this.displayRectangles.clear()\n        this.displayRectangles.putAll(displayRectangles)\n        this.originalRectangles.clear()\n        this.originalRectangles.putAll(originalRectangles)\n\n        this.displayPolygons.clear()\n        this.displayPolygons.putAll(displayPolygons)\n        this.originalPolygons.clear()\n        this.originalPolygons.putAll(originalPolygons)\n\n        this.displayTexts.clear()\n        this.displayTexts.putAll(displayTexts)\n        this.originalTexts.clear()\n        this.originalTexts.putAll(originalTexts)\n        \n        markDirty()\n    }\n\n    data class ShapeLayerSnapshot(\n        val displayLines: Map<Offset, Shape.Line>,\n        val originalLines: Map<Offset, Shape.Line>,\n        val displayCircles: Map<Offset, Shape.Circle>,\n        val originalCircles: Map<Offset, Shape.Circle>,\n        val displayTriangles: Map<Offset, Shape.Triangle>,\n        val originalTriangles: Map<Offset, Shape.Triangle>,\n        val displayRectangles: Map<Offset, Shape.Rectangle>,\n        val originalRectangles: Map<Offset, Shape.Rectangle>,\n        val displayPolygons: Map<Offset, Shape.Polygon>,\n        val originalPolygons: Map<Offset, Shape.Polygon>,\n        val displayTexts: Map<Offset, Shape.Text>,\n        val originalTexts: Map<Offset, Shape.Text>\n    )\n}\n\n\n\n"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/shapedrawing/layer/SpecialLayerHelper.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.shapedrawing.layer\n\nimport androidx.compose.ui.graphics.ImageBitmap\nimport java.util.UUID\n\n/**\n * 专门处理特殊图层（如背景层）的辅助类\n * 集中管理背景层的查找、创建、更新等操作\n */\nclass SpecialLayerHelper(\n    private val layerManager: LayerManager,\n    private val backgroundLayerName: String = \"背景图层\"\n) {\n    \n    private var cachedBackgroundLayer: ImageLayer? = null\n    private var cachedBackgroundLayerId: UUID? = null\n\n    /**\n     * 获取背景层，如果不存在则返回 null\n     * 使用缓存机制优化性能，自动验证缓存有效性\n     */\n    fun getBackgroundLayer(): ImageLayer? {\n        // 先验证缓存是否有效\n        val cached = validateBackgroundLayerCache()\n        if (cached != null) {\n            return cached\n        }\n        \n        // 缓存失效或不存在，重新查找\n        val found = layerManager.layers.value\n            .firstOrNull { it.name == backgroundLayerName && it is ImageLayer } as? ImageLayer\n        \n        cachedBackgroundLayer = found\n        cachedBackgroundLayerId = found?.id\n        return found\n    }\n    \n    /**\n     * 获取或创建背景层\n     * 如果不存在则创建新的背景层并添加到索引 0\n     */\n    fun getOrCreateBackgroundLayer(image: ImageBitmap): ImageLayer {\n        val existing = getBackgroundLayer()\n        if (existing != null) {\n            return existing\n        }\n        \n        val newLayer = ImageLayer(backgroundLayerName, image)\n        layerManager.addLayer(newLayer, index = 0)\n        cachedBackgroundLayer = newLayer\n        cachedBackgroundLayerId = newLayer.id\n        return newLayer\n    }\n    \n    /**\n     * 更新背景层图像\n     * 如果背景层不存在，则创建新的\n     */\n    fun updateBackgroundLayer(image: ImageBitmap) {\n        val layer = getOrCreateBackgroundLayer(image)\n        layer.updateImage(image)\n    }\n    \n    /**\n     * 检查是否存在背景层\n     */\n    fun hasBackgroundLayer(): Boolean {\n        return getBackgroundLayer() != null\n    }\n    \n    /**\n     * 移除背景层（谨慎使用，通常不应该删除背景层）\n     * 此方法主要用于清理或重置场景\n     */\n    fun removeBackgroundLayer(): Boolean {\n        val backgroundLayer = getBackgroundLayer()\n        return if (backgroundLayer != null) {\n            layerManager.removeLayer(backgroundLayer.id) != null\n        } else {\n            false\n        }\n    }\n    \n    /**\n     * 获取背景层的尺寸（如果存在）\n     */\n    fun getBackgroundSize(): Pair<Float, Float>? {\n        return getBackgroundLayer()?.image?.let { \n            Pair(it.width.toFloat(), it.height.toFloat()) \n        }\n    }\n\n    /**\n     * 验证背景层缓存是否仍然有效\n     */\n    private fun validateBackgroundLayerCache(): ImageLayer? {\n        val cachedId = cachedBackgroundLayerId ?: return null\n        val cached = cachedBackgroundLayer\n        \n        // 检查缓存的背景层是否仍在图层列表中\n        if (cached != null && cached.id == cachedId && \n            layerManager.layers.value.any { it.id == cachedId }) {\n            return cached\n        }\n        \n        // 缓存失效，清空缓存\n        cachedBackgroundLayer = null\n        cachedBackgroundLayerId = null\n        return null\n    }\n}\n\n"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/shapedrawing/model/Shape.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.shapedrawing.model\n\nimport androidx.compose.ui.geometry.Offset\nimport kotlin.math.sqrt\nimport kotlin.math.pow\nimport kotlin.math.PI\n\n/**\n * 形状枚举类型\n * \n * @author Tony Shen\n * @date 2024/11/22 14:34\n * @version V1.0\n */\nenum class ShapeEnum {\n    Point,\n    Line,\n    Circle,\n    Triangle,\n    Rectangle,\n    Polygon,\n    Text,\n    NotAShape\n}\n\n/**\n * 形状基类密封类\n * 定义了所有支持的几何形状类型\n */\nsealed class Shape {\n    \n    /**\n     * 线条形状\n     * @param from 起始点\n     * @param to 结束点\n     * @param shapeProperties 形状属性\n     */\n    data class Line(\n        val from: Offset, \n        val to: Offset, \n        val shapeProperties: ShapeProperties\n    ) : Shape() {\n        \n        /**\n         * 检查线条是否有效\n         */\n        fun isValid(): Boolean = from != Offset.Unspecified && to != Offset.Unspecified\n        \n        /**\n         * 获取线条长度\n         */\n        fun getLength(): Float = if (isValid()) {\n            sqrt((to.x - from.x).pow(2) + (to.y - from.y).pow(2))\n        } else 0f\n    }\n\n    /**\n     * 圆形形状\n     * @param center 圆心\n     * @param radius 半径\n     * @param shapeProperties 形状属性\n     */\n    data class Circle(\n        val center: Offset, \n        val radius: Float, \n        val shapeProperties: ShapeProperties\n    ) : Shape() {\n        \n        /**\n         * 检查圆形是否有效\n         */\n        fun isValid(): Boolean = center != Offset.Unspecified && radius > 0\n        \n        /**\n         * 获取圆形面积\n         */\n        fun getArea(): Float = if (isValid()) {\n            (PI * radius * radius).toFloat()\n        } else 0f\n    }\n\n    /**\n     * 三角形形状\n     * @param first 第一个顶点\n     * @param second 第二个顶点\n     * @param third 第三个顶点\n     * @param shapeProperties 形状属性\n     */\n    data class Triangle(\n        val first: Offset, \n        val second: Offset? = null, \n        val third: Offset? = null, \n        val shapeProperties: ShapeProperties\n    ) : Shape() {\n        \n        /**\n         * 检查三角形是否有效\n         */\n        fun isValid(): Boolean = \n            first != Offset.Unspecified && \n            second != Offset.Unspecified && \n            third != Offset.Unspecified\n        \n        /**\n         * 获取三角形顶点列表\n         */\n        fun getPoints(): List<Offset> = listOfNotNull(first, second, third)\n    }\n\n    /**\n     * 矩形形状\n     * @param tl 左上角\n     * @param bl 左下角\n     * @param br 右下角\n     * @param tr 右上角\n     * @param rectFirst 第一个点（用于预览）\n     * @param shapeProperties 形状属性\n     */\n    data class Rectangle(\n        val tl: Offset, \n        val bl: Offset, \n        val br: Offset, \n        val tr: Offset, \n        val rectFirst: Offset,\n        val shapeProperties: ShapeProperties\n    ) : Shape() {\n        \n        /**\n         * 检查矩形是否有效\n         */\n        fun isValid(): Boolean = \n            tl != Offset.Unspecified && \n            bl != Offset.Unspecified && \n            br != Offset.Unspecified && \n            tr != Offset.Unspecified\n        \n        /**\n         * 获取矩形顶点列表\n         */\n        fun getPoints(): List<Offset> = listOf(tl, bl, br, tr)\n        \n        /**\n         * 获取矩形宽度\n         */\n        fun getWidth(): Float = if (isValid()) {\n            sqrt((tr.x - tl.x).pow(2) + (tr.y - tl.y).pow(2))\n        } else 0f\n        \n        /**\n         * 获取矩形高度\n         */\n        fun getHeight(): Float = if (isValid()) {\n            sqrt((bl.x - tl.x).pow(2) + (bl.y - tl.y).pow(2))\n        } else 0f\n    }\n\n    /**\n     * 多边形形状\n     * @param points 顶点列表\n     * @param shapeProperties 形状属性\n     */\n    data class Polygon(\n        val points: List<Offset>, \n        val shapeProperties: ShapeProperties\n    ) : Shape() {\n        \n        /**\n         * 检查多边形是否有效\n         */\n        fun isValid(): Boolean = points.size >= 3 && \n            points.all { it != Offset.Unspecified }\n        \n        /**\n         * 获取多边形的边数\n         */\n        fun getSideCount(): Int = points.size\n    }\n\n    /**\n     * 文本形状\n     * @param point 文本位置\n     * @param message 文本内容\n     * @param shapeProperties 形状属性\n     */\n    data class Text(\n        val point: Offset, \n        val message: String, \n        val shapeProperties: ShapeProperties\n    ) : Shape() {\n        \n        /**\n         * 检查文本是否有效\n         */\n        fun isValid(): Boolean = point != Offset.Unspecified && message.isNotBlank()\n    }\n}\n\n/**\n * 扩展函数：获取形状类型\n */\nfun Shape.getType(): ShapeEnum = when (this) {\n    is Shape.Line -> ShapeEnum.Line\n    is Shape.Circle -> ShapeEnum.Circle\n    is Shape.Triangle -> ShapeEnum.Triangle\n    is Shape.Rectangle -> ShapeEnum.Rectangle\n    is Shape.Polygon -> ShapeEnum.Polygon\n    is Shape.Text -> ShapeEnum.Text\n}\n\n"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/shapedrawing/model/ShapeProperties.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.shapedrawing.model\n\nimport androidx.compose.ui.graphics.Color\nimport cn.netdiscovery.monica.ui.controlpanel.shapedrawing.geometry.Border\n\n/**\n * 形状属性类\n * 定义了形状的视觉属性，如颜色、透明度、字体大小等\n * \n * @author Tony Shen\n * @date 2024/11/24 14:18\n * @version V1.0\n */\ndata class ShapeProperties(\n    val color: Color = Color.Red,\n    val alpha: Float = 1f,\n    val fontSize: Float = 40f,\n    val fill: Boolean = false,\n    val border: Border = Border.Line\n) {\n    \n    init {\n        require(alpha in 0f..1f) { \"Alpha must be between 0.0 and 1.0\" }\n        require(fontSize > 0f) { \"Font size must be positive\" }\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/shapedrawing/state/ShapeDrawingState.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.shapedrawing.state\n\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.graphics.Color\nimport cn.netdiscovery.monica.ui.controlpanel.shapedrawing.model.Shape\nimport cn.netdiscovery.monica.ui.controlpanel.shapedrawing.model.Shape.*\nimport cn.netdiscovery.monica.ui.controlpanel.shapedrawing.model.ShapeEnum\nimport cn.netdiscovery.monica.ui.controlpanel.shapedrawing.model.ShapeProperties\nimport cn.netdiscovery.monica.ui.widget.image.gesture.MotionEvent\n\n/**\n * 形状绘制状态管理\n * 统一管理所有形状的状态，降低耦合度\n * \n * @author Tony Shen\n * @date 2025/9/8\n * @version V1.0\n */\n@Stable\nclass ShapeDrawingState {\n    \n    // 当前选择的形状类型\n    var currentShape by mutableStateOf(ShapeEnum.NotAShape)\n        private set\n    \n    // 当前形状属性\n    var currentShapeProperty by mutableStateOf(ShapeProperties())\n        private set\n    \n    // 当前鼠标位置\n    var currentPosition by mutableStateOf(Offset.Unspecified)\n        private set\n    \n    var previousPosition by mutableStateOf(Offset.Unspecified)\n        private set\n    \n    // 当前运动事件\n    var motionEvent by mutableStateOf(MotionEvent.Idle)\n        private set\n    \n    // 线段相关状态\n    var currentLineStart by mutableStateOf(Offset.Unspecified)\n        private set\n    var currentLineEnd by mutableStateOf(Offset.Unspecified)\n        private set\n    \n    // 圆形相关状态\n    var currentCircleCenter by mutableStateOf(Offset.Unspecified)\n        private set\n    var currentCircleRadius by mutableStateOf(0.0f)\n        private set\n    \n    // 三角形相关状态\n    var currentTriangleFirst by mutableStateOf(Offset.Unspecified)\n        private set\n    var currentTriangleSecond by mutableStateOf(Offset.Unspecified)\n        private set\n    var currentTriangleThird by mutableStateOf(Offset.Unspecified)\n        private set\n    \n    // 矩形相关状态\n    var currentRectFirst by mutableStateOf(Offset.Unspecified)\n        private set\n    var currentRectTL by mutableStateOf(Offset.Unspecified)\n        private set\n    var currentRectBR by mutableStateOf(Offset.Unspecified)\n        private set\n    var currentRectTR by mutableStateOf(Offset.Unspecified)\n        private set\n    var currentRectBL by mutableStateOf(Offset.Unspecified)\n        private set\n    \n    // 多边形相关状态\n    var currentPolygonFirst by mutableStateOf(Offset.Unspecified)\n        private set\n    var currentPolygonPoints = mutableStateListOf<Offset>()\n    \n    // 文字相关状态\n    var currentText by mutableStateOf(\"\")\n        private set\n    \n    // 已完成的形状集合\n    val displayLines = mutableStateMapOf<Offset, Line>()\n    val originalLines = mutableStateMapOf<Offset, Line>()\n    val displayCircles = mutableStateMapOf<Offset, Circle>()\n    val originalCircles = mutableStateMapOf<Offset, Circle>()\n    val displayTriangles = mutableStateMapOf<Offset, Triangle>()\n    val originalTriangles = mutableStateMapOf<Offset, Triangle>()\n    val displayRectangles = mutableStateMapOf<Offset, Rectangle>()\n    val originalRectangles = mutableStateMapOf<Offset, Rectangle>()\n    val displayPolygons = mutableStateMapOf<Offset, Polygon>()\n    val originalPolygons = mutableStateMapOf<Offset, Polygon>()\n    val displayTexts = mutableStateMapOf<Offset, Text>()\n    val originalTexts = mutableStateMapOf<Offset, Text>()\n    \n    // 最后一个绘制的形状跟踪\n    var lastDrawnShapeKey by mutableStateOf<Offset?>(null)\n        private set\n    var lastDrawnShapeType by mutableStateOf<String?>(null)\n        private set\n    \n    /**\n     * 设置当前形状类型\n     */\n    fun selectShape(shape: ShapeEnum) {\n        currentShape = shape\n        clearCurrentDrawingState()\n    }\n    \n    /**\n     * 更新形状属性\n     */\n    fun updateShapeProperty(property: ShapeProperties) {\n        currentShapeProperty = property\n    }\n    \n    /**\n     * 更新颜色\n     */\n    fun updateColor(color: Color) {\n        currentShapeProperty = currentShapeProperty.copy(color = color)\n    }\n    \n    /**\n     * 更新位置信息\n     */\n    fun updatePosition(position: Offset) {\n        previousPosition = currentPosition\n        currentPosition = position\n    }\n    \n    /**\n     * 更新运动事件\n     */\n    fun updateMotionEvent(event: MotionEvent) {\n        motionEvent = event\n    }\n    \n    /**\n     * 更新线段状态\n     */\n    fun updateLineState(start: Offset? = null, end: Offset? = null) {\n        start?.let { currentLineStart = it }\n        end?.let { currentLineEnd = it }\n    }\n    \n    /**\n     * 更新圆形状态\n     */\n    fun updateCircleState(center: Offset? = null, radius: Float? = null) {\n        center?.let { currentCircleCenter = it }\n        radius?.let { currentCircleRadius = it }\n    }\n    \n    /**\n     * 更新三角形状态\n     */\n    fun updateTriangleState(first: Offset? = null, second: Offset? = null, third: Offset? = null) {\n        first?.let { currentTriangleFirst = it }\n        second?.let { currentTriangleSecond = it }\n        third?.let { currentTriangleThird = it }\n    }\n    \n    /**\n     * 更新矩形状态\n     */\n    fun updateRectangleState(\n        first: Offset? = null,\n        tl: Offset? = null,\n        br: Offset? = null,\n        tr: Offset? = null,\n        bl: Offset? = null\n    ) {\n        first?.let { currentRectFirst = it }\n        tl?.let { currentRectTL = it }\n        br?.let { currentRectBR = it }\n        tr?.let { currentRectTR = it }\n        bl?.let { currentRectBL = it }\n    }\n    \n    /**\n     * 更新多边形状态\n     */\n    fun updatePolygonState(first: Offset? = null, addPoint: Offset? = null) {\n        first?.let { currentPolygonFirst = it }\n        addPoint?.let { currentPolygonPoints.add(it) }\n    }\n    \n    /**\n     * 更新文字状态\n     */\n    fun updateTextState(text: String) {\n        currentText = text\n    }\n    \n    /**\n     * 记录最后绘制的形状\n     */\n    fun recordLastDrawnShape(key: Offset, type: String) {\n        lastDrawnShapeKey = key\n        lastDrawnShapeType = type\n    }\n    \n    /**\n     * 清除当前绘制状态（保留已完成的形状）\n     */\n    fun clearCurrentDrawingState() {\n        // 保存当前颜色设置\n        val currentColor = currentShapeProperty.color\n        \n        // 清除临时绘制状态\n        currentLineStart = Offset.Unspecified\n        currentLineEnd = Offset.Unspecified\n        \n        currentCircleCenter = Offset.Unspecified\n        currentCircleRadius = 0.0f\n        \n        currentTriangleFirst = Offset.Unspecified\n        currentTriangleSecond = Offset.Unspecified\n        currentTriangleThird = Offset.Unspecified\n        \n        currentRectFirst = Offset.Unspecified\n        currentRectTL = Offset.Unspecified\n        currentRectBR = Offset.Unspecified\n        currentRectTR = Offset.Unspecified\n        currentRectBL = Offset.Unspecified\n        \n        currentPolygonFirst = Offset.Unspecified\n        currentPolygonPoints.clear()\n        \n        currentText = \"\"\n        \n        // 重置最后一个形状的跟踪\n        lastDrawnShapeKey = null\n        lastDrawnShapeType = null\n        \n        // 保持颜色设置\n        currentShapeProperty = currentShapeProperty.copy(color = currentColor)\n    }\n    \n    /**\n     * 清除所有已完成的形状\n     */\n    fun clearAllShapes() {\n        // 清理已完成的形状\n        displayLines.clear()\n        originalLines.clear()\n        displayCircles.clear()\n        originalCircles.clear()\n        displayTriangles.clear()\n        originalTriangles.clear()\n        displayRectangles.clear()\n        originalRectangles.clear()\n        displayPolygons.clear()\n        originalPolygons.clear()\n        displayTexts.clear()\n        originalTexts.clear()\n        \n        // 清理当前绘制状态\n        clearCurrentDrawingState()\n        \n        // 重置最后一个形状的跟踪\n        lastDrawnShapeKey = null\n        lastDrawnShapeType = null\n    }\n    \n    /**\n     * 添加形状到显示和原始集合\n     */\n    fun addShape(key: Offset, displayShape: Shape, originalShape: Shape) {\n        when (displayShape) {\n            is Line -> {\n                displayLines[key] = displayShape\n                originalLines[key] = originalShape as Line\n            }\n            is Circle -> {\n                displayCircles[key] = displayShape\n                originalCircles[key] = originalShape as Circle\n            }\n            is Triangle -> {\n                displayTriangles[key] = displayShape\n                originalTriangles[key] = originalShape as Triangle\n            }\n            is Rectangle -> {\n                displayRectangles[key] = displayShape\n                originalRectangles[key] = originalShape as Rectangle\n            }\n            is Polygon -> {\n                displayPolygons[key] = displayShape\n                originalPolygons[key] = originalShape as Polygon\n            }\n            is Text -> {\n                displayTexts[key] = displayShape\n                originalTexts[key] = originalShape as Text\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/shapedrawing/widget/CanvasView.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.shapedrawing.widget\n\nimport androidx.compose.foundation.Canvas\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.key\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.drawWithCache\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.geometry.Size\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.drawscope.DrawScope\nimport cn.netdiscovery.monica.ui.controlpanel.shapedrawing.EditorController\nimport cn.netdiscovery.monica.ui.controlpanel.shapedrawing.animation.ShapeAnimationManager\nimport cn.netdiscovery.monica.ui.controlpanel.shapedrawing.layer.ImageLayer\nimport cn.netdiscovery.monica.ui.controlpanel.shapedrawing.layer.Layer\nimport cn.netdiscovery.monica.ui.controlpanel.shapedrawing.layer.ShapeLayer\nimport cn.netdiscovery.monica.ui.controlpanel.shapedrawing.model.Shape\nimport cn.netdiscovery.monica.ui.controlpanel.shapedrawing.state.ShapeDrawingState\nimport kotlin.math.PI\nimport kotlin.math.sin\n\n@Composable\nfun CanvasView(\n    editorController: EditorController,\n    drawingState: ShapeDrawingState,\n    animationManager: ShapeAnimationManager,\n    modifier: Modifier = Modifier.Companion,\n    overlay: DrawScope.() -> Unit = {},\n    showImageLayerControls: Boolean = true\n) {\n    // 观察图层列表变化，触发重组和重绘\n    val layers by editorController.layerManager.layers.collectAsState()\n    val activeLayer by editorController.layerManager.activeLayer.collectAsState()\n\n    Box(modifier = modifier) {\n        // 为每个图层创建独立的缓存层\n        // 使用 key() 确保只有版本变化的图层才重组\n        layers.forEach { layer ->\n            if (!layer.visible || layer.opacity <= 0f) return@forEach\n            \n            key(layer.id, layer.version) {\n                // 使用 drawWithCache 缓存图层绘制内容\n                Canvas(\n                    modifier = Modifier\n                        .fillMaxSize()\n                        .drawWithCache {\n                            // 缓存图层绘制内容\n                            // 注意：layer 在 lambda 中被捕获，但由于使用了 key()，只有版本变化时才会重新创建\n                            onDrawBehind {\n                                // 绘制单个图层\n                                editorController.layerRenderer.drawLayer(this, layer)\n                            }\n                        },\n                    onDraw = {\n                        // 空的绘制函数，实际绘制在 drawWithCache 的 onDrawBehind 中\n                    }\n                )\n            }\n        }\n        \n        // 绘制动画和覆盖层（这些不需要缓存，因为它们是动态的）\n        Canvas(modifier = Modifier.fillMaxSize()) {\n            drawAllAnimations(\n                animationManager = animationManager,\n                displayLines = drawingState.displayLines,\n                displayCircles = drawingState.displayCircles,\n                displayTriangles = drawingState.displayTriangles,\n                displayRectangles = drawingState.displayRectangles,\n                displayPolygons = drawingState.displayPolygons\n            )\n            \n            // 绘制激活图像层的控制点\n            if (showImageLayerControls) {\n                val activeImageLayer = activeLayer as? ImageLayer\n                if (activeImageLayer != null && !activeImageLayer.locked && \n                    !editorController.isBackgroundLayer(activeImageLayer)) {\n                    // 获取背景图尺寸（如果存在）\n                    val backgroundSize = editorController.getBackgroundSize()\n                    \n                    ImageLayerControlRenderer.drawControls(\n                        drawScope = this,\n                        layer = activeImageLayer,\n                        canvasWidth = size.width,\n                        canvasHeight = size.height,\n                        backgroundSize = backgroundSize\n                    )\n                }\n            }\n            \n            overlay()\n        }\n    }\n}\n\nprivate fun DrawScope.drawAllAnimations(\n    animationManager: ShapeAnimationManager,\n    displayLines: Map<Offset, Shape.Line>,\n    displayCircles: Map<Offset, Shape.Circle>,\n    displayTriangles: Map<Offset, Shape.Triangle>,\n    displayRectangles: Map<Offset, Shape.Rectangle>,\n    displayPolygons: Map<Offset, Shape.Polygon>\n) {\n    val currentTime = System.currentTimeMillis()\n\n    animationManager.animatedShapes.forEach { (_, animatedShape) ->\n        val elapsed = currentTime - animatedShape.startTime\n        val progress = (elapsed.toFloat() / animatedShape.duration.toFloat()).coerceIn(0f, 1f)\n\n        if (progress < 1f) {\n            val easedProgress = animationManager.easeInOutCubic(progress)\n            val scale = animationManager.lerp(animatedShape.startScale, animatedShape.endScale, easedProgress)\n            val alpha = animationManager.lerp(animatedShape.startAlpha, animatedShape.endAlpha, easedProgress)\n\n            val key = animatedShape.key\n            val highlightColor = animatedShape.highlightColor\n\n            val parts = key.split(\"_\")\n            if (parts.size >= 3) {\n                val x = parts[1].toFloatOrNull() ?: 0f\n                val y = parts[2].toFloatOrNull() ?: 0f\n                val center = Offset(x, y)\n\n                val pulseAlpha = alpha * (0.5f + 0.5f * sin((progress * PI * 4).toDouble()).toFloat())\n\n                drawCircle(\n                    color = highlightColor.copy(alpha = pulseAlpha * 0.3f),\n                    radius = 30f * scale,\n                    center = center\n                )\n                drawCircle(\n                    color = Color.Companion.White.copy(alpha = pulseAlpha * 0.6f),\n                    radius = 20f * scale,\n                    center = center\n                )\n                drawCircle(\n                    color = highlightColor.copy(alpha = pulseAlpha * 0.8f),\n                    radius = 10f * scale,\n                    center = center\n                )\n            }\n        }\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/shapedrawing/widget/DraggableTextField.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.shapedrawing.widget\n\nimport androidx.compose.foundation.ExperimentalFoundationApi\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.gestures.detectDragGestures\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.TextField\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.draw.shadow\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.input.pointer.pointerInput\nimport androidx.compose.ui.unit.Density\nimport androidx.compose.ui.unit.IntOffset\nimport androidx.compose.ui.unit.dp\nimport cn.netdiscovery.monica.ui.widget.confirmButton\nimport kotlin.math.abs\nimport kotlin.math.roundToInt\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.shapedrawing.widget.DraggableTextField\n * @author: Tony Shen\n * @date: 2024/11/26 10:28\n * @version: V1.0 <描述当前版本功能>\n */\n@OptIn(ExperimentalFoundationApi::class)\n@Composable\nfun draggableTextField(\n    modifier: Modifier = Modifier,\n    text: String,\n    canvasWidthPx: Float,\n    canvasHeightPx: Float,\n    density: Density,\n    onTextChanged: (String) -> Unit,\n    onDragged: (Offset) -> Unit\n) {\n    var offset by remember { mutableStateOf(Offset.Zero) }\n    \n    // 计算画布中心（相对于屏幕中心）\n    val halfCanvasWidthPx = canvasWidthPx / 2f\n    val halfCanvasHeightPx = canvasHeightPx / 2f\n    \n    // 文本输入框的尺寸（像素）\n    val textFieldWidthPx = with(density) { 250.dp.toPx() }\n    val textFieldHeightPx = with(density) { 130.dp.toPx() }\n    val halfTextFieldWidthPx = textFieldWidthPx / 2f\n    val halfTextFieldHeightPx = textFieldHeightPx / 2f\n\n    Box(\n        modifier = modifier\n            .offset { IntOffset(offset.x.roundToInt(), offset.y.roundToInt()) }\n            .pointerInput(Unit) {\n                detectDragGestures { change ->\n                    offset += change\n                    // 限制拖拽范围在画布区域内\n                    // offset 是相对于屏幕中心的偏移，需要确保文本输入框不超出画布边界\n                    if (abs(offset.x) > halfCanvasWidthPx - halfTextFieldWidthPx || \n                        abs(offset.y) > halfCanvasHeightPx - halfTextFieldHeightPx) {\n                        offset -= change\n                        return@detectDragGestures\n                    }\n                }\n            }\n            .shadow(8.dp)\n            .background(Color.White)\n            .padding(16.dp)\n            .fillMaxWidth()\n            .wrapContentHeight(Alignment.Top)\n            .clip(RoundedCornerShape(8.dp))\n    ) {\n        Column {\n            TextField (\n                value = text,\n                onValueChange = onTextChanged,\n                modifier = Modifier.width(220.dp)\n            )\n\n            confirmButton(true, modifier = Modifier.align(Alignment.End).padding(top = 5.dp)) {\n                onDragged.invoke(offset)\n            }\n        }\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/shapedrawing/widget/ImageLayerControlRenderer.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.shapedrawing.widget\n\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.geometry.Rect\nimport androidx.compose.ui.geometry.Size\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.drawscope.DrawScope\nimport androidx.compose.ui.graphics.drawscope.Stroke\nimport androidx.compose.ui.unit.dp\nimport cn.netdiscovery.monica.ui.controlpanel.shapedrawing.EditorController\nimport cn.netdiscovery.monica.ui.controlpanel.shapedrawing.layer.ImageLayer\nimport kotlin.math.cos\nimport kotlin.math.sin\n\n/**\n * 图像层控制点渲染器\n * 负责绘制图像层的控制点、旋转手柄和边界框\n */\nobject ImageLayerControlRenderer {\n    \n    // 控制点大小（像素）\n    private const val CONTROL_POINT_SIZE = 8f\n    // 旋转手柄长度（像素）\n    private const val ROTATION_HANDLE_LENGTH = 30f\n    // 控制点颜色\n    private val CONTROL_POINT_COLOR = Color(0xFF2196F3)\n    // 旋转手柄颜色\n    private val ROTATION_HANDLE_COLOR = Color(0xFF4CAF50)\n    // 边界框颜色\n    private val BOUNDARY_COLOR = Color(0xFF2196F3)\n    \n    /**\n     * 计算图像层的边界框（考虑变换后的位置）\n     */\n    fun calculateImageBounds(\n        layer: ImageLayer,\n        canvasWidth: Float,\n        canvasHeight: Float,\n        backgroundSize: Pair<Float, Float>? = null\n    ): Rect? {\n        val bitmap = layer.image ?: return null\n        \n        if (bitmap.width <= 0 || bitmap.height <= 0) return null\n        \n        // 判断是否为背景图层（使用常量，避免硬编码）\n        val isBackgroundLayer = layer.name == EditorController.BACKGROUND_LAYER_NAME\n        \n        if (isBackgroundLayer) {\n            // 背景图层填充整个画布\n            return Rect(Offset.Zero, Size(canvasWidth, canvasHeight))\n        }\n        \n        // 计算适应和居中后的尺寸（与 ImageLayer.render() 逻辑一致）\n        // 如果提供了背景层尺寸，需要考虑背景层在画布上的显示比例\n        val fitScale = if (backgroundSize != null) {\n            val referenceWidth = backgroundSize.first\n            val referenceHeight = backgroundSize.second\n            \n            // 计算背景层在画布上的缩放比例（背景层被拉伸到画布尺寸）\n            val backgroundScaleX = canvasWidth / referenceWidth\n            val backgroundScaleY = canvasHeight / referenceHeight\n            \n            // 计算基于背景层原始尺寸的缩放比例，确保图像尺寸不超过背景层尺寸\n            val referenceScaleX = referenceWidth / bitmap.width\n            val referenceScaleY = referenceHeight / bitmap.height\n            val referenceFitScale = minOf(referenceScaleX, referenceScaleY).coerceAtMost(1f)\n            \n            // 图像层在背景层原始坐标系中的缩放比例\n            // 然后需要乘以背景层在画布上的缩放比例，得到在画布坐标系中的缩放比例\n            referenceFitScale * minOf(backgroundScaleX, backgroundScaleY)\n        } else {\n            // 没有背景层时，基于画布尺寸\n            val canvasScaleX = canvasWidth / bitmap.width\n            val canvasScaleY = canvasHeight / bitmap.height\n            minOf(canvasScaleX, canvasScaleY).coerceAtMost(1f)\n        }\n        \n        val scaledWidth = bitmap.width * fitScale\n        val scaledHeight = bitmap.height * fitScale\n        \n        val centerOffsetX = (canvasWidth - scaledWidth) / 2f\n        val centerOffsetY = (canvasHeight - scaledHeight) / 2f\n        \n        // 应用用户定义的变换\n        val transform = layer.transform\n        \n        // 计算变换后的四个角点\n        // 注意：在 ImageLayer.render() 中，withTransform 的执行顺序是（从外到内，后写的先执行）：\n        // 1. 自动平移（centerOffset）- 最外层，最后执行\n        // 2. 自动缩放（fitScale）- 将图像坐标系转换到适应后的坐标系\n        // 3. 用户平移（在适应后的坐标系中，相对于适应后图像中心）\n        // 4. 用户旋转（相对于 adaptedPivot，在适应后的坐标系中）\n        // 5. 用户缩放（相对于 adaptedPivot，在适应后的坐标系中）- 最内层，最先执行\n        \n        // 先计算原始图像的四个角点（在图像坐标系中，0,0 到 width,height）\n        val imageTopLeft = Offset(0f, 0f)\n        val imageTopRight = Offset(bitmap.width.toFloat(), 0f)\n        val imageBottomLeft = Offset(0f, bitmap.height.toFloat())\n        val imageBottomRight = Offset(bitmap.width.toFloat(), bitmap.height.toFloat())\n        val imageCenter = Offset(bitmap.width / 2f, bitmap.height / 2f)\n        \n        // 计算 pivot（在图像坐标系中）\n        val pivot = if (transform.pivot == Offset.Zero) {\n            imageCenter\n        } else {\n            imageCenter + transform.pivot\n        }\n        \n        // 将 pivot 转换到适应后的坐标系（用于用户变换）\n        // 注意：在 ImageLayer.render() 中，用户变换是在适应后的坐标系中进行的\n        // 所以这里需要先将图像坐标转换到适应后的坐标系，然后再应用用户变换\n        val adaptedPivot = Offset(pivot.x * fitScale, pivot.y * fitScale)\n        \n        // 计算变换后的角点\n        // 变换顺序：用户平移(translation) -> fitScale -> centerOffset\n        // translation 在图像原始坐标系中，所以需要先应用 translation，然后应用 fitScale\n        \n        // 1. 应用用户平移（在图像原始坐标系中）\n        val translation = transform.translation\n        val translatedTopLeft = imageTopLeft + translation\n        val translatedTopRight = imageTopRight + translation\n        val translatedBottomLeft = imageBottomLeft + translation\n        val translatedBottomRight = imageBottomRight + translation\n        \n        // 2. 应用用户缩放（相对于 pivot，在图像原始坐标系中）\n        val scaleXFinal = transform.scaleX\n        val scaleYFinal = transform.scaleY\n        val scaledTopLeft = applyScale(translatedTopLeft, pivot, scaleXFinal, scaleYFinal)\n        val scaledTopRight = applyScale(translatedTopRight, pivot, scaleXFinal, scaleYFinal)\n        val scaledBottomLeft = applyScale(translatedBottomLeft, pivot, scaleXFinal, scaleYFinal)\n        val scaledBottomRight = applyScale(translatedBottomRight, pivot, scaleXFinal, scaleYFinal)\n        \n        // 3. 应用用户旋转（相对于 pivot，在图像原始坐标系中）\n        val rotation = transform.rotation\n        val rotatedTopLeft = applyRotation(scaledTopLeft, pivot, rotation)\n        val rotatedTopRight = applyRotation(scaledTopRight, pivot, rotation)\n        val rotatedBottomLeft = applyRotation(scaledBottomLeft, pivot, rotation)\n        val rotatedBottomRight = applyRotation(scaledBottomRight, pivot, rotation)\n        \n        // 4. 应用自动缩放（fitScale）- 将图像坐标系转换到适应后的坐标系\n        val adaptedTopLeft = Offset(rotatedTopLeft.x * fitScale, rotatedTopLeft.y * fitScale)\n        val adaptedTopRight = Offset(rotatedTopRight.x * fitScale, rotatedTopRight.y * fitScale)\n        val adaptedBottomLeft = Offset(rotatedBottomLeft.x * fitScale, rotatedBottomLeft.y * fitScale)\n        val adaptedBottomRight = Offset(rotatedBottomRight.x * fitScale, rotatedBottomRight.y * fitScale)\n        \n        // 5. 应用自动平移（centerOffset）- 在画布坐标系中\n        val finalTopLeft = adaptedTopLeft + Offset(centerOffsetX, centerOffsetY)\n        val finalTopRight = adaptedTopRight + Offset(centerOffsetX, centerOffsetY)\n        val finalBottomLeft = adaptedBottomLeft + Offset(centerOffsetX, centerOffsetY)\n        val finalBottomRight = adaptedBottomRight + Offset(centerOffsetX, centerOffsetY)\n        \n        // 计算边界框\n        val minX = minOf(finalTopLeft.x, finalTopRight.x, finalBottomLeft.x, finalBottomRight.x)\n        val maxX = maxOf(finalTopLeft.x, finalTopRight.x, finalBottomLeft.x, finalBottomRight.x)\n        val minY = minOf(finalTopLeft.y, finalTopRight.y, finalBottomLeft.y, finalBottomRight.y)\n        val maxY = maxOf(finalTopLeft.y, finalTopRight.y, finalBottomLeft.y, finalBottomRight.y)\n        \n        return Rect(\n            offset = Offset(minX, minY),\n            size = Size(maxX - minX, maxY - minY)\n        )\n    }\n    \n    /**\n     * 计算图像层的中心点（考虑变换后）\n     */\n    fun calculateImageCenter(\n        layer: ImageLayer,\n        canvasWidth: Float,\n        canvasHeight: Float,\n        backgroundSize: Pair<Float, Float>? = null\n    ): Offset? {\n        val bounds = calculateImageBounds(layer, canvasWidth, canvasHeight, backgroundSize) ?: return null\n        return Offset(bounds.center.x, bounds.center.y)\n    }\n    \n    /**\n     * 计算旋转手柄的位置\n     */\n    fun calculateRotationHandlePosition(\n        layer: ImageLayer,\n        canvasWidth: Float,\n        canvasHeight: Float,\n        backgroundSize: Pair<Float, Float>? = null\n    ): Offset? {\n        val center = calculateImageCenter(layer, canvasWidth, canvasHeight, backgroundSize) ?: return null\n        val bounds = calculateImageBounds(layer, canvasWidth, canvasHeight, backgroundSize) ?: return null\n        \n        // 旋转手柄在图像上方中心\n        val handleOffset = Offset(0f, -bounds.height / 2f - ROTATION_HANDLE_LENGTH)\n        \n        return center + handleOffset\n    }\n    \n    /**\n     * 计算所有控制点的位置（四个角、旋转手柄和裁剪控制点）\n     */\n    fun calculateControlPoints(\n        layer: ImageLayer,\n        canvasWidth: Float,\n        canvasHeight: Float,\n        backgroundSize: Pair<Float, Float>? = null\n    ): List<ControlPoint> {\n        val bounds = calculateImageBounds(layer, canvasWidth, canvasHeight, backgroundSize) ?: return emptyList()\n        val rotationHandle = calculateRotationHandlePosition(layer, canvasWidth, canvasHeight, backgroundSize)\n        \n        val points = mutableListOf<ControlPoint>()\n        \n        // 四个角的控制点\n        points.add(ControlPoint(ControlPointType.CORNER_TOP_LEFT, bounds.topLeft))\n        points.add(ControlPoint(ControlPointType.CORNER_TOP_RIGHT, bounds.topRight))\n        points.add(ControlPoint(ControlPointType.CORNER_BOTTOM_LEFT, bounds.bottomLeft))\n        points.add(ControlPoint(ControlPointType.CORNER_BOTTOM_RIGHT, bounds.bottomRight))\n        \n        // 旋转手柄\n        if (rotationHandle != null) {\n            points.add(ControlPoint(ControlPointType.ROTATION_HANDLE, rotationHandle))\n        }\n        \n        // 裁剪控制点（如果存在裁剪区域）\n        // 注意：裁剪控制点的计算比较复杂，需要考虑所有变换\n        // 这里先简化实现，后续可以优化\n        val cropRect = layer.transform.cropRect\n        if (cropRect != null) {\n            // 裁剪区域在图像坐标系中，需要转换到画布坐标系\n            // 简化实现：使用 bounds 作为参考，后续可以完善\n            // TODO: 完善裁剪控制点的计算，考虑所有变换\n        }\n        \n        return points\n    }\n    \n    /**\n     * 绘制图像层的控制点和边界框\n     */\n    fun drawControls(\n        drawScope: DrawScope,\n        layer: ImageLayer,\n        canvasWidth: Float,\n        canvasHeight: Float,\n        backgroundSize: Pair<Float, Float>? = null\n    ) {\n        val bounds = calculateImageBounds(layer, canvasWidth, canvasHeight, backgroundSize) ?: return\n        val controlPoints = calculateControlPoints(layer, canvasWidth, canvasHeight, backgroundSize)\n        \n        // 绘制边界框\n        drawScope.drawRect(\n            color = BOUNDARY_COLOR.copy(alpha = 0.5f),\n            style = Stroke(width = 1f),\n            topLeft = bounds.topLeft,\n            size = bounds.size\n        )\n        \n        // 绘制控制点\n        controlPoints.forEach { point ->\n            val color = when (point.type) {\n                ControlPointType.ROTATION_HANDLE -> ROTATION_HANDLE_COLOR\n                else -> CONTROL_POINT_COLOR\n            }\n            \n            drawScope.drawCircle(\n                color = color,\n                radius = CONTROL_POINT_SIZE,\n                center = point.position\n            )\n            \n            // 绘制控制点外圈\n            drawScope.drawCircle(\n                color = Color.White,\n                radius = CONTROL_POINT_SIZE + 1f,\n                style = Stroke(width = 1f),\n                center = point.position\n            )\n        }\n        \n        // 绘制旋转手柄连线\n        val rotationHandle = controlPoints.find { it.type == ControlPointType.ROTATION_HANDLE }\n        val center = calculateImageCenter(layer, canvasWidth, canvasHeight, backgroundSize)\n        if (rotationHandle != null && center != null) {\n            drawScope.drawLine(\n                color = ROTATION_HANDLE_COLOR.copy(alpha = 0.5f),\n                start = center,\n                end = rotationHandle.position,\n                strokeWidth = 1f\n            )\n        }\n    }\n    \n    /**\n     * 检查点是否在控制点附近\n     */\n    fun hitTestControlPoint(\n        point: Offset,\n        layer: ImageLayer,\n        canvasWidth: Float,\n        canvasHeight: Float,\n        backgroundSize: Pair<Float, Float>? = null\n    ): ControlPoint? {\n        val controlPoints = calculateControlPoints(layer, canvasWidth, canvasHeight, backgroundSize)\n        val hitRadius = CONTROL_POINT_SIZE * 2f\n        \n        return controlPoints.firstOrNull { controlPoint ->\n            val distance = (point - controlPoint.position).getDistance()\n            distance <= hitRadius\n        }\n    }\n    \n    // 辅助函数：应用缩放\n    private fun applyScale(point: Offset, pivot: Offset, scaleX: Float, scaleY: Float): Offset {\n        val translated = point - pivot\n        val scaled = Offset(translated.x * scaleX, translated.y * scaleY)\n        return scaled + pivot\n    }\n    \n    // 辅助函数：应用旋转\n    private fun applyRotation(point: Offset, pivot: Offset, rotation: Float): Offset {\n        val translated = point - pivot\n        val rad = Math.toRadians(rotation.toDouble())\n        val cos = cos(rad).toFloat()\n        val sin = sin(rad).toFloat()\n        val rotated = Offset(\n            translated.x * cos - translated.y * sin,\n            translated.x * sin + translated.y * cos\n        )\n        return rotated + pivot\n    }\n}\n\n/**\n * 控制点类型\n */\nenum class ControlPointType {\n    CORNER_TOP_LEFT,\n    CORNER_TOP_RIGHT,\n    CORNER_BOTTOM_LEFT,\n    CORNER_BOTTOM_RIGHT,\n    ROTATION_HANDLE,\n    CROP_TOP_LEFT,\n    CROP_TOP_RIGHT,\n    CROP_BOTTOM_LEFT,\n    CROP_BOTTOM_RIGHT,\n    CROP_TOP,\n    CROP_BOTTOM,\n    CROP_LEFT,\n    CROP_RIGHT\n}\n\n/**\n * 控制点数据\n */\ndata class ControlPoint(\n    val type: ControlPointType,\n    val position: Offset\n)\n\n"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/shapedrawing/widget/LayerPanel.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.shapedrawing.widget\n\nimport androidx.compose.foundation.BorderStroke\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.gestures.detectDragGestures\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.ButtonDefaults\nimport androidx.compose.material.Checkbox\nimport androidx.compose.material.Icon\nimport androidx.compose.material.IconButton\nimport androidx.compose.material.MaterialTheme\nimport androidx.compose.material.OutlinedButton\nimport androidx.compose.material.OutlinedTextField\nimport androidx.compose.material.Surface\nimport androidx.compose.material.Text\nimport androidx.compose.material.AlertDialog\nimport androidx.compose.material.TextButton\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Add\nimport androidx.compose.material.icons.filled.Delete\nimport androidx.compose.material.icons.filled.Edit\nimport androidx.compose.material.icons.filled.Lock\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.toComposeImageBitmap\nimport androidx.compose.ui.input.pointer.pointerInput\nimport androidx.compose.ui.zIndex\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.unit.dp\nimport cn.netdiscovery.monica.ui.controlpanel.shapedrawing.EditorController\nimport cn.netdiscovery.monica.ui.controlpanel.shapedrawing.layer.LayerType\nimport cn.netdiscovery.monica.state.ApplicationState\nimport cn.netdiscovery.monica.utils.chooseImage\nimport cn.netdiscovery.monica.utils.getBufferedImage\nimport org.slf4j.LoggerFactory\nimport java.util.UUID\nimport kotlin.collections.asReversed\n\n@Composable\nfun LayerPanel(\n    editorController: EditorController,\n    state: ApplicationState,\n    modifier: Modifier = Modifier.Companion\n) {\n    val density = LocalDensity.current\n    val layers by editorController.layerManager.layers.collectAsState()\n    val activeLayer by editorController.layerManager.activeLayer.collectAsState()\n\n    var editingLayerId by remember { mutableStateOf<UUID?>(null) }\n    var editingName by remember { mutableStateOf(\"\") }\n    var deleteConfirmLayerId by remember { mutableStateOf<UUID?>(null) }\n    var draggedLayerId by remember { mutableStateOf<UUID?>(null) }\n    var dragOffset by remember { mutableStateOf(0f) }\n\n    val displayLayers = remember(layers) { layers.asReversed() }\n    val logger = remember { LoggerFactory.getLogger(object : Any() {}.javaClass.enclosingClass) }\n\n    Column(\n        modifier = modifier.padding(12.dp),\n        verticalArrangement = Arrangement.spacedBy(8.dp)\n    ) {\n        Text(\n            text = \"图层\",\n            style = MaterialTheme.typography.h6,\n            modifier = Modifier.Companion.padding(bottom = 4.dp)\n        )\n\n        val shapeLayerCount = layers.count { it.type == LayerType.SHAPE }\n        val canAddShapeLayer = editorController.canAddShapeLayer()\n\n        Row(\n            modifier = Modifier.Companion.fillMaxWidth(),\n            horizontalArrangement = Arrangement.spacedBy(8.dp)\n        ) {\n            OutlinedButton(\n                onClick = {\n                    val result = editorController.addShapeLayer(\"形状图层\")\n                    if (result == null && !canAddShapeLayer) {\n                        state.showTray(\"最多只能创建 1 个形状层\", \"提示\")\n                    }\n                },\n                modifier = Modifier.Companion.weight(1f),\n                enabled = canAddShapeLayer,\n                colors = ButtonDefaults.outlinedButtonColors(\n                    contentColor = MaterialTheme.colors.primary\n                )\n            ) {\n                Icon(\n                    Icons.Default.Add,\n                    contentDescription = null,\n                    modifier = Modifier.Companion.size(14.dp)\n                )\n                Spacer(modifier = Modifier.Companion.width(4.dp))\n                Text(\n                    if (canAddShapeLayer) \"形状层\" else \"已达上限\",\n                    style = MaterialTheme.typography.caption\n                )\n            }\n\n            OutlinedButton(\n                onClick = {\n                    chooseImage(state) { file ->\n                        try {\n                            var bufferedImage = getBufferedImage(file, state)\n                            \n                            // 如果添加的图像超过背景图，自动缩放\n                            val bgSize = editorController.getBackgroundSize()\n                            if (bgSize != null) {\n                                val bgWidth = bgSize.first.toInt()\n                                val bgHeight = bgSize.second.toInt()\n                                \n                                // 如果图像超过背景层大小，缩放到不超过背景层\n                                if (bufferedImage.width > bgWidth || bufferedImage.height > bgHeight) {\n                                    val scaleX = bgWidth.toFloat() / bufferedImage.width\n                                    val scaleY = bgHeight.toFloat() / bufferedImage.height\n                                    val scale = minOf(scaleX, scaleY)\n                                    \n                                    val newWidth = (bufferedImage.width * scale).toInt()\n                                    val newHeight = (bufferedImage.height * scale).toInt()\n                                    \n                                    val scaledImage = java.awt.Image.SCALE_SMOOTH\n                                    val resizedBufImage = bufferedImage.getScaledInstance(newWidth, newHeight, scaledImage)\n                                    bufferedImage = java.awt.image.BufferedImage(newWidth, newHeight, java.awt.image.BufferedImage.TYPE_INT_RGB)\n                                    val g2d = bufferedImage.createGraphics()\n                                    g2d.drawImage(resizedBufImage, 0, 0, null)\n                                    g2d.dispose()\n                                    \n                                    logger.info(\"自动缩放图像: ${bufferedImage.width}x${bufferedImage.height} (原始: ${getBufferedImage(file, state).width}x${getBufferedImage(file, state).height})\")\n                                }\n                            }\n                            \n                            val imageBitmap = bufferedImage.toComposeImageBitmap()\n                            val imageLayerCount = layers.count { it.type == LayerType.IMAGE } + 1\n                            val layerName = \"图像图层 $imageLayerCount\"\n                            editorController.createImageLayer(layerName, imageBitmap)\n                            logger.info(\"成功添加图像层: $layerName, 文件: ${file.name}\")\n                        } catch (e: Exception) {\n                            logger.error(\"添加图像层失败: ${file.name}\", e)\n                            state.showTray(\"添加图像层失败: ${e.message}\", \"错误\")\n                        }\n                    }\n                },\n                modifier = Modifier.Companion.weight(1f),\n                colors = ButtonDefaults.outlinedButtonColors(\n                    contentColor = MaterialTheme.colors.primary\n                )\n            ) {\n                Icon(\n                    Icons.Default.Add,\n                    contentDescription = null,\n                    modifier = Modifier.Companion.size(14.dp)\n                )\n                Spacer(modifier = Modifier.Companion.width(4.dp))\n                Text(\"图像层\", style = MaterialTheme.typography.caption)\n            }\n        }\n\n        Spacer(modifier = Modifier.Companion.height(4.dp))\n\n        Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {\n            displayLayers.forEachIndexed { displayIndex, layer ->\n                val actualIndex = layers.indexOfFirst { it.id == layer.id }\n                val isActive = activeLayer?.id == layer.id\n                val isDragging = draggedLayerId == layer.id\n                val upEnabled = actualIndex < layers.lastIndex\n                val downEnabled = actualIndex > 0\n                val cardColor = when {\n                    isDragging -> MaterialTheme.colors.primary.copy(alpha = 0.15f)\n                    isActive -> MaterialTheme.colors.primary.copy(alpha = 0.08f)\n                    else -> MaterialTheme.colors.surface\n                }\n                val borderColor = when {\n                    isDragging -> MaterialTheme.colors.primary.copy(alpha = 0.6f)\n                    isActive -> MaterialTheme.colors.primary.copy(alpha = 0.4f)\n                    else -> MaterialTheme.colors.onSurface.copy(alpha = 0.08f)\n                }\n\n                Surface(\n                    shape = RoundedCornerShape(10.dp),\n                    color = cardColor,\n                    border = BorderStroke(\n                        width = if (isDragging || isActive) 2.dp else 1.dp,\n                        color = borderColor\n                    ),\n                    elevation = if (isDragging) 8.dp else 0.dp,\n                    modifier = Modifier.Companion\n                        .fillMaxWidth()\n                        .zIndex(if (isDragging) 1f else 0f)\n                        .pointerInput(layer.id, layers.size, density.density) {\n                            val itemHeightPx = with(density) { 80.dp.toPx() }\n                            val threshold = itemHeightPx * 0.5f\n                            \n                            detectDragGestures(\n                                onDragStart = {\n                                    draggedLayerId = layer.id\n                                    dragOffset = 0f\n                                },\n                                onDrag = { change, dragAmount ->\n                                    dragOffset += dragAmount.y\n                                    \n                                    // 重新计算当前索引（因为 layers 可能已更新）\n                                    val currentLayers = editorController.layerManager.layers.value\n                                    val currentIndex = currentLayers.indexOfFirst { it.id == layer.id }\n                                    \n                                    // 注意：displayLayers 是反转的，所以向下拖拽（dragOffset > 0）应该向上移动（index 增加）\n                                    if (dragOffset > threshold && currentIndex < currentLayers.lastIndex) {\n                                        // 向下拖拽，在列表中向上移动（index 增加）\n                                        val targetIndex = currentIndex + 1\n                                        editorController.layerManager.moveLayerTo(layer.id, targetIndex)\n                                        dragOffset = 0f\n                                    } else if (dragOffset < -threshold && currentIndex > 0) {\n                                        // 向上拖拽，在列表中向下移动（index 减少）\n                                        val targetIndex = currentIndex - 1\n                                        editorController.layerManager.moveLayerTo(layer.id, targetIndex)\n                                        dragOffset = 0f\n                                    }\n                                },\n                                onDragEnd = {\n                                    draggedLayerId = null\n                                    dragOffset = 0f\n                                }\n                            )\n                        }\n                ) {\n                    Column(\n                        modifier = Modifier.Companion\n                            .clickable { editorController.setActiveLayer(layer.id) }\n                            .padding(10.dp)\n                    ) {\n                        Row(\n                            verticalAlignment = Alignment.Companion.CenterVertically,\n                            horizontalArrangement = Arrangement.spacedBy(10.dp)\n                        ) {\n                            // 可见性复选框\n                            Checkbox(\n                                checked = layer.visible,\n                                onCheckedChange = { checked ->\n                                    editorController.layerManager.setLayerVisibility(layer.id, checked)\n                                },\n                                modifier = Modifier.Companion.size(20.dp)\n                            )\n\n                            // 图层类型图标\n                            Box(\n                                modifier = Modifier.Companion.size(32.dp),\n                                contentAlignment = Alignment.Companion.Center\n                            ) {\n                                Surface(\n                                    shape = androidx.compose.foundation.shape.RoundedCornerShape(6.dp),\n                                    color = if (isActive) {\n                                        MaterialTheme.colors.primary.copy(alpha = 0.1f)\n                                    } else {\n                                        MaterialTheme.colors.onSurface.copy(alpha = 0.05f)\n                                    },\n                                    modifier = Modifier.Companion.size(32.dp)\n                                ) {\n                                    Box(contentAlignment = Alignment.Companion.Center) {\n                                        Text(\n                                            text = if (layer.type == LayerType.IMAGE) \"图\" else \"形\",\n                                            style = MaterialTheme.typography.caption.copy(\n                                                color = if (isActive) {\n                                                    MaterialTheme.colors.primary\n                                                } else {\n                                                    MaterialTheme.colors.onSurface.copy(alpha = 0.6f)\n                                                },\n                                                fontWeight = FontWeight.Companion.Bold\n                                            )\n                                        )\n                                    }\n                                }\n                            }\n\n                            // 图层信息\n                            Column(\n                                modifier = Modifier.Companion.weight(1f),\n                                verticalArrangement = Arrangement.spacedBy(2.dp)\n                            ) {\n                                if (editingLayerId == layer.id) {\n                                    OutlinedTextField(\n                                        value = editingName,\n                                        onValueChange = { editingName = it },\n                                        singleLine = true,\n                                        modifier = Modifier.Companion.fillMaxWidth(),\n                                        textStyle = MaterialTheme.typography.body2\n                                    )\n                                } else {\n                                    Text(\n                                        text = layer.name,\n                                        style = MaterialTheme.typography.body2.copy(\n                                            color = if (isActive) {\n                                                MaterialTheme.colors.primary\n                                            } else {\n                                                MaterialTheme.colors.onSurface\n                                            },\n                                            fontWeight = if (isActive) FontWeight.Companion.SemiBold else FontWeight.Companion.Normal\n                                        ),\n                                        maxLines = 1,\n                                        overflow = TextOverflow.Companion.Ellipsis\n                                    )\n                                }\n                                Row(\n                                    verticalAlignment = Alignment.Companion.CenterVertically,\n                                    horizontalArrangement = Arrangement.spacedBy(6.dp)\n                                ) {\n                                    Text(\n                                        text = layer.type.toDisplayName(),\n                                        style = MaterialTheme.typography.caption.copy(\n                                            color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)\n                                        )\n                                    )\n                                    if (layer.type == LayerType.SHAPE && isActive) {\n                                        Text(\n                                            text = \"• 当前绘制\",\n                                            style = MaterialTheme.typography.caption.copy(\n                                                color = MaterialTheme.colors.primary,\n                                                fontWeight = FontWeight.Companion.Bold\n                                            )\n                                        )\n                                    }\n                                }\n                            }\n                        }\n\n                        // 操作按钮区域\n                        if (editingLayerId == layer.id) {\n                            Row(\n                                modifier = Modifier.Companion\n                                    .fillMaxWidth()\n                                    .padding(top = 8.dp),\n                                horizontalArrangement = Arrangement.spacedBy(6.dp)\n                            ) {\n                                OutlinedButton(\n                                    onClick = {\n                                        editorController.layerManager.renameLayer(\n                                            layer.id,\n                                            editingName.trim().ifEmpty { layer.name })\n                                        editingLayerId = null\n                                    },\n                                    modifier = Modifier.Companion.weight(1f),\n                                    colors = ButtonDefaults.outlinedButtonColors(\n                                        contentColor = MaterialTheme.colors.primary\n                                    )\n                                ) {\n                                    Text(\"保存\", style = MaterialTheme.typography.caption)\n                                }\n                                OutlinedButton(\n                                    onClick = { editingLayerId = null },\n                                    modifier = Modifier.Companion.weight(1f),\n                                    colors = ButtonDefaults.outlinedButtonColors(\n                                        contentColor = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)\n                                    )\n                                ) {\n                                    Text(\"取消\", style = MaterialTheme.typography.caption)\n                                }\n                            }\n                        } else {\n                            Row(\n                                modifier = Modifier.Companion\n                                    .fillMaxWidth()\n                                    .padding(top = 8.dp),\n                                horizontalArrangement = Arrangement.spacedBy(4.dp)\n                            ) {\n                                IconButton(\n                                    onClick = {\n                                        editorController.layerManager.setLayerLocked(layer.id, !layer.locked)\n                                    },\n                                    modifier = Modifier.Companion.size(32.dp)\n                                ) {\n                                    Icon(\n                                        Icons.Default.Lock,\n                                        contentDescription = if (layer.locked) \"解锁\" else \"锁定\",\n                                        modifier = Modifier.Companion.size(16.dp),\n                                        tint = if (layer.locked) {\n                                            MaterialTheme.colors.error\n                                        } else {\n                                            MaterialTheme.colors.onSurface.copy(alpha = 0.6f)\n                                        }\n                                    )\n                                }\n                                IconButton(\n                                    onClick = {\n                                        editingLayerId = layer.id\n                                        editingName = layer.name\n                                    },\n                                    modifier = Modifier.Companion.size(32.dp)\n                                ) {\n                                    Icon(\n                                        Icons.Default.Edit,\n                                        contentDescription = \"重命名\",\n                                        modifier = Modifier.Companion.size(16.dp),\n                                        tint = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)\n                                    )\n                                }\n                                IconButton(\n                                    onClick = { editorController.layerManager.moveLayerUp(layer.id) },\n                                    enabled = upEnabled,\n                                    modifier = Modifier.Companion.size(32.dp)\n                                ) {\n                                    Text(\n                                        \"↑\",\n                                        style = MaterialTheme.typography.caption,\n                                        color = if (upEnabled) {\n                                            MaterialTheme.colors.onSurface.copy(alpha = 0.6f)\n                                        } else {\n                                            MaterialTheme.colors.onSurface.copy(alpha = 0.3f)\n                                        }\n                                    )\n                                }\n                                IconButton(\n                                    onClick = { editorController.layerManager.moveLayerDown(layer.id) },\n                                    enabled = downEnabled,\n                                    modifier = Modifier.Companion.size(32.dp)\n                                ) {\n                                    Text(\n                                        \"↓\",\n                                        style = MaterialTheme.typography.caption,\n                                        color = if (downEnabled) {\n                                            MaterialTheme.colors.onSurface.copy(alpha = 0.6f)\n                                        } else {\n                                            MaterialTheme.colors.onSurface.copy(alpha = 0.3f)\n                                        }\n                                    )\n                                }\n                                IconButton(\n                                    onClick = {\n                                        if (editorController.isBackgroundLayer(layer)) {\n                                            state.showTray(\"无法删除背景图层\", \"提示\")\n                                        } else {\n                                            deleteConfirmLayerId = layer.id\n                                        }\n                                    },\n                                    modifier = Modifier.Companion.size(32.dp)\n                                ) {\n                                    Icon(\n                                        Icons.Default.Delete,\n                                        contentDescription = \"删除\",\n                                        modifier = Modifier.Companion.size(16.dp),\n                                        tint = MaterialTheme.colors.error.copy(alpha = 0.7f)\n                                    )\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    // 删除确认对话框\n    deleteConfirmLayerId?.let { layerId ->\n        val layerToDelete = layers.firstOrNull { it.id == layerId }\n        if (layerToDelete != null) {\n            AlertDialog(\n                onDismissRequest = { deleteConfirmLayerId = null },\n                title = {\n                    Text(\"确认删除\")\n                },\n                text = {\n                    Text(\"确定要删除图层 \\\"${layerToDelete.name}\\\" 吗？此操作无法撤销。\")\n                },\n                confirmButton = {\n                    TextButton(\n                        onClick = {\n                            editorController.removeLayer(layerId)\n                            deleteConfirmLayerId = null\n                        }\n                    ) {\n                        Text(\"删除\", color = MaterialTheme.colors.error)\n                    }\n                },\n                dismissButton = {\n                    TextButton(\n                        onClick = { deleteConfirmLayerId = null }\n                    ) {\n                        Text(\"取消\")\n                    }\n                }\n            )\n        }\n    }\n}\n\nprivate fun LayerType.toDisplayName(): String = when (this) {\n    LayerType.IMAGE -> \"图像层\"\n    LayerType.SHAPE -> \"形状层\"\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/shapedrawing/widget/ShapeDrawingPropertiesMenuDialog.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.shapedrawing.widget\n\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.Card\nimport androidx.compose.material.Slider\nimport androidx.compose.material.SliderDefaults\nimport androidx.compose.material.Text\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport androidx.compose.ui.window.Dialog\nimport cn.netdiscovery.monica.ui.controlpanel.shapedrawing.geometry.Border\nimport cn.netdiscovery.monica.ui.controlpanel.shapedrawing.model.ShapeProperties\nimport cn.netdiscovery.monica.ui.widget.properties.ExposedSelectionMenu\nimport cn.netdiscovery.monica.i18n.getCurrentStringResource\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.shapedrawing.widget.ShapeDrawingPropertiesMenuDialog\n * @author: Tony Shen\n * @date: 2024/11/26 10:33\n * @version: V1.0 <描述当前版本功能>\n */\n@Composable\nfun ShapeDrawingPropertiesMenuDialog(\n    shapeProperties: ShapeProperties, \n    onDismiss: (ShapeProperties) -> Unit\n) {\n    val i18nState = getCurrentStringResource()\n    var alpha    by remember { mutableStateOf(shapeProperties.alpha) }\n    var fontSize by remember { mutableStateOf(shapeProperties.fontSize) }\n    var fill     by remember { mutableStateOf(shapeProperties.fill) }\n    var border   by remember { mutableStateOf(shapeProperties.border) }\n\n    Dialog(onDismissRequest = {\n        // 返回更新后的属性\n        val updatedProperties = shapeProperties.copy(\n            alpha = alpha,\n            fontSize = fontSize,\n            fill = fill,\n            border = border\n        )\n        onDismiss(updatedProperties)\n    }) {\n\n        Card(\n            elevation = 2.dp,\n            shape = RoundedCornerShape(8.dp),\n            modifier = Modifier.padding(vertical = 8.dp)\n        ) {\n            Column(modifier = Modifier.padding(8.dp)) {\n\n                Text(\n                    text = i18nState.get(\"alpha\") + \": ${alpha}\",\n                    fontSize = 16.sp,\n                    modifier = Modifier.padding(horizontal = 12.dp)\n                )\n\n                Slider(\n                    value = alpha,\n                    onValueChange = {\n                        alpha = it\n                    },\n                    valueRange = 0f..1f,\n                    onValueChangeFinished = {},\n                    colors = SliderDefaults.colors()\n                )\n\n                Text(\n                    text = i18nState.get(\"font_size\") + \": ${fontSize.toInt()}\",\n                    fontSize = 16.sp,\n                    modifier = Modifier.padding(horizontal = 12.dp)\n                )\n\n                Slider(\n                    value = fontSize,\n                    onValueChange = {\n                        fontSize = it\n                    },\n                    valueRange = 1f..100f,\n                    onValueChangeFinished = {},\n                    colors = SliderDefaults.colors()\n                )\n\n                ExposedSelectionMenu(title = i18nState.get(\"fill\"),\n                    index = when (fill) {\n                        false -> 0\n                        true -> 1\n                    },\n                    options = listOf(\"False\", \"True\"),\n                    onSelected = {\n                        fill = when (it) {\n                            0 -> false\n                            1 -> true\n                            else -> false\n                        }\n                    }\n                )\n\n                ExposedSelectionMenu(title = i18nState.get(\"border\"),\n                    index = when (border) {\n                        Border.No      -> 0\n                        Border.Dot     -> 1\n                        Border.Dash    -> 2\n                        Border.DashDot -> 3\n                        Border.Line    -> 4\n                    },\n                    options = listOf(\"No\", \"Dot\", \"Dash\", \"DashDot\", \"Line\"),\n                    onSelected = {\n                        border = when (it) {\n                            0 -> Border.No\n                            1 -> Border.Dot\n                            2 -> Border.Dash\n                            3 -> Border.DashDot\n                            4 -> Border.Line\n                            else -> Border.No\n                        }\n                    }\n                )\n            }\n        }\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/shapedrawing/widget/TextDrawer.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.shapedrawing.widget\n\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.graphics.Canvas\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.nativeCanvas\nimport androidx.compose.ui.graphics.toArgb\nimport org.jetbrains.skia.Font\nimport org.jetbrains.skia.Paint\nimport org.jetbrains.skia.TextLine\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.controlpanel.shapedrawing.widget.TextDrawerImpl\n * @author: Tony Shen\n * @date: 2024/11/20 11:59\n * @version: V1.0 <描述当前版本功能>\n */\nobject TextDrawer: cn.netdiscovery.monica.ui.controlpanel.shapedrawing.geometry.TextDrawer {\n\n    override fun text(canvas: Canvas, pos: Offset, text: List<String>, color: Color, fontSize: Float) {\n        println(\"=== TextDrawer.text 开始 ===\")\n        println(\"传入位置: $pos\")\n        println(\"文本内容: $text\")\n        println(\"字体大小: $fontSize\")\n        \n        val paint = Paint()\n        paint.color = color.toArgb()\n        val font = Font(null, fontSize)\n        val subscript = Font(null, fontSize - 10)\n        var current = pos.x\n        \n        text.forEachIndexed { index, str ->\n            val line = TextLine.make(str, if (index % 2 == 0) font else subscript)\n            \n            // 让文字显示在控件的中心位置\n            // pos.x 是控件中心，我们需要减去文字宽度的一半来水平居中\n            val textWidth = line.width\n            val drawX = pos.x - textWidth / 2\n            \n            // 计算文本基线位置\n            // pos.y 是文本的中心位置，我们希望文字垂直居中显示\n            // 由于Skia的drawTextLine中Y坐标是基线位置，我们需要计算正确的基线\n            val fontHeight = if (index % 2 == 0) fontSize else (fontSize - 10)\n            \n            // 文字应该垂直居中显示在 pos.y 位置\n            // 对于大多数字体，基线大约在字体高度的 70-80% 位置\n            // 要让文本中心在 pos.y，基线应该在 pos.y + (fontHeight * 0.3) 左右\n            // 但考虑到不同字体的差异，使用更精确的计算：\n            // 文本中心 = 基线 - fontHeight * 0.7\n            // 所以：基线 = 文本中心 + fontHeight * 0.7 = pos.y + fontHeight * 0.7\n            // 但这样会让文本偏下，应该使用：基线 = pos.y + fontHeight * 0.3\n            val drawY = pos.y + fontHeight * 0.3f\n            \n            println(\"绘制文本: '$str' 在 ($drawX, $drawY)\")\n            println(\"  - 原始位置: $pos\")\n            println(\"  - 文字宽度: $textWidth\")\n            println(\"  - 字体高度: $fontHeight\")\n            println(\"  - X居中计算: pos.x(${pos.x}) - textWidth/2(${textWidth/2}) = $drawX\")\n            println(\"  - Y基线计算: pos.y(${pos.y}) + fontHeight*0.3(${fontHeight*0.3f}) = $drawY\")\n            \n            canvas.nativeCanvas.drawTextLine(line, drawX, drawY, paint)\n            current += line.width\n        }\n        println(\"=== TextDrawer.text 结束 ===\")\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/webscreenshot/WebScreenshotView.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.webscreenshot\n\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.*\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.unit.dp\nimport cn.netdiscovery.monica.exception.ErrorSeverity\nimport cn.netdiscovery.monica.exception.ErrorType\nimport cn.netdiscovery.monica.exception.showError\nimport cn.netdiscovery.monica.state.ApplicationState\nimport cn.netdiscovery.monica.ui.i18n.rememberI18nState\nimport cn.netdiscovery.monica.ui.widget.*\nimport cn.netdiscovery.monica.utils.WebScreenshotOptions\nimport cn.netdiscovery.monica.utils.checkNodeInstalled\nimport org.koin.compose.koinInject\n\n/**\n * 网页截图视图\n * \n * @author: Tony Shen\n * @date: 2026/01/12\n * @version: V1.0\n */\n@Composable\nfun webScreenshot(state: ApplicationState) {\n    val i18nState = rememberI18nState()\n    val viewModel: WebScreenshotViewModel = koinInject()\n\n    var urlText by remember { mutableStateOf(\"https://\") }\n    var fullPage by remember { mutableStateOf(true) }\n    var waitUntil by remember { mutableStateOf(\"networkidle\") }\n    var timeoutText by remember { mutableStateOf(\"30000\") }\n    var viewportWidthText by remember { mutableStateOf(\"\") }\n    var viewportHeightText by remember { mutableStateOf(\"\") }\n    var clarityScaleText by remember { mutableStateOf(\"2.0\") }\n    \n    var nodeInstalled by remember { mutableStateOf(false) }\n    \n    // 检查 Node.js 环境\n    LaunchedEffect(Unit) {\n        nodeInstalled = checkNodeInstalled()\n    }\n\n    Column(\n        modifier = Modifier\n            .fillMaxSize()\n            .padding(16.dp),\n        horizontalAlignment = Alignment.CenterHorizontally,\n        verticalArrangement = Arrangement.spacedBy(16.dp)\n    ) {\n        // 标题\n        title(\n            modifier = Modifier.padding(bottom = 8.dp),\n            text = i18nState.getString(\"web_screenshot\"),\n            color = MaterialTheme.colors.onBackground\n        )\n\n        // Node.js 环境检查提示\n        if (!nodeInstalled) {\n            Card(\n                modifier = Modifier.fillMaxWidth(),\n                backgroundColor = MaterialTheme.colors.error.copy(alpha = 0.1f),\n                shape = RoundedCornerShape(8.dp)\n            ) {\n                Column(\n                    modifier = Modifier.padding(16.dp),\n                    horizontalAlignment = Alignment.CenterHorizontally\n                ) {\n                    Text(\n                        text = i18nState.getString(\"nodejs_not_installed\"),\n                        color = MaterialTheme.colors.error,\n                        style = MaterialTheme.typography.body2\n                    )\n                    Spacer(modifier = Modifier.height(8.dp))\n                    Text(\n                        text = i18nState.getString(\"please_install_nodejs\"),\n                        color = MaterialTheme.colors.onSurface.copy(alpha = 0.7f),\n                        style = MaterialTheme.typography.caption\n                    )\n                }\n            }\n        }\n\n        // URL 输入框\n        Card(\n            modifier = Modifier.fillMaxWidth(),\n            shape = RoundedCornerShape(8.dp),\n            elevation = 2.dp\n        ) {\n            Column(\n                modifier = Modifier.padding(16.dp),\n                verticalArrangement = Arrangement.spacedBy(12.dp)\n            ) {\n                Text(\n                    text = i18nState.getString(\"website_url\"),\n                    style = MaterialTheme.typography.subtitle1\n                )\n                \n                OutlinedTextField(\n                    value = urlText,\n                    onValueChange = { urlText = it },\n                    modifier = Modifier.fillMaxWidth(),\n                    label = { Text(i18nState.getString(\"enter_url\")) },\n                    placeholder = { Text(\"https://example.com\") },\n                    singleLine = true\n                )\n\n                // 截图选项\n                Divider(modifier = Modifier.padding(vertical = 8.dp))\n                \n                Text(\n                    text = i18nState.getString(\"screenshot_options\"),\n                    style = MaterialTheme.typography.subtitle2\n                )\n\n                // 全页截图选项\n                Row(\n                    modifier = Modifier.fillMaxWidth(),\n                    horizontalArrangement = Arrangement.SpaceBetween,\n                    verticalAlignment = Alignment.CenterVertically\n                ) {\n                    Text(text = i18nState.getString(\"full_page_screenshot\"))\n                    Switch(\n                        checked = fullPage,\n                        onCheckedChange = { fullPage = it }\n                    )\n                }\n\n                // 等待策略\n                Row(\n                    modifier = Modifier.fillMaxWidth(),\n                    horizontalArrangement = Arrangement.SpaceBetween,\n                    verticalAlignment = Alignment.CenterVertically\n                ) {\n                    Text(text = i18nState.getString(\"wait_until\"))\n                    var expanded by remember { mutableStateOf(false) }\n                    Box {\n                        Button(\n                            onClick = { expanded = true },\n                            modifier = Modifier.width(150.dp)\n                        ) {\n                            Text(waitUntil, style = MaterialTheme.typography.body2)\n                        }\n                        DropdownMenu(\n                            expanded = expanded,\n                            onDismissRequest = { expanded = false }\n                        ) {\n                            listOf(\"load\", \"domcontentloaded\", \"networkidle\").forEach { option ->\n                                DropdownMenuItem(onClick = {\n                                    waitUntil = option\n                                    expanded = false\n                                }) {\n                                    Text(option)\n                                }\n                            }\n                        }\n                    }\n                }\n\n                // 超时设置\n                basicTextFieldWithTitle(\n                    titleText = i18nState.getString(\"timeout_ms\"),\n                    value = timeoutText,\n                    width = 120.dp,\n                    onValueChange = { timeoutText = it }\n                )\n\n                // 视口尺寸（可选）\n                Row(\n                    modifier = Modifier.fillMaxWidth(),\n                    horizontalArrangement = Arrangement.spacedBy(8.dp)\n                ) {\n                    basicTextFieldWithTitle(\n                        titleText = i18nState.getString(\"viewport_width\"),\n                        value = viewportWidthText,\n                        width = 100.dp,\n                        onValueChange = { viewportWidthText = it }\n                    )\n                    basicTextFieldWithTitle(\n                        titleText = i18nState.getString(\"viewport_height\"),\n                        value = viewportHeightText,\n                        width = 100.dp,\n                        onValueChange = { viewportHeightText = it }\n                    )\n                }\n\n                basicTextFieldWithTitle(\n                    titleText = i18nState.getString(\"screenshot_clarity\"),\n                    value = clarityScaleText,\n                    width = 120.dp,\n                    onValueChange = { clarityScaleText = it }\n                )\n            }\n        }\n\n        // 操作按钮\n        Row(\n            modifier = Modifier.fillMaxWidth(),\n            horizontalArrangement = Arrangement.spacedBy(12.dp)\n        ) {\n            Button(\n                onClick = {\n                    if (!nodeInstalled) {\n                        showError(\n                            ErrorType.NETWORK_ERROR,\n                            ErrorSeverity.MEDIUM,\n                            i18nState.getString(\"nodejs_not_installed\"),\n                            i18nState.getString(\"please_install_nodejs\")\n                        )\n                        return@Button\n                    }\n\n                    if (urlText.isBlank() || !urlText.startsWith(\"http\")) {\n                        showError(\n                            ErrorType.VALIDATION_ERROR,\n                            ErrorSeverity.LOW,\n                            i18nState.getString(\"invalid_url\"),\n                            i18nState.getString(\"please_enter_valid_url\")\n                        )\n                        return@Button\n                    }\n\n                    val clarityScale = clarityScaleText.toDoubleOrNull()\n                    if (clarityScale == null || clarityScale <= 0.0 || clarityScale > 4.0) {\n                        showError(\n                            ErrorType.VALIDATION_ERROR,\n                            ErrorSeverity.LOW,\n                            i18nState.getString(\"invalid_screenshot_clarity\"),\n                            i18nState.getString(\"please_enter_valid_screenshot_clarity\")\n                        )\n                        return@Button\n                    }\n\n                    val options = WebScreenshotOptions(\n                        fullPage = fullPage,\n                        waitUntil = waitUntil,\n                        timeout = timeoutText.toLongOrNull() ?: 30000L,\n                        viewportWidth = viewportWidthText.toIntOrNull(),\n                        viewportHeight = viewportHeightText.toIntOrNull(),\n                        deviceScaleFactor = clarityScale\n                    )\n\n                    viewModel.captureWebScreenshot(state, urlText.trim(), options)\n                },\n                modifier = Modifier.weight(1f),\n                enabled = nodeInstalled && urlText.isNotBlank()\n            ) {\n                Text(i18nState.getString(\"capture_screenshot\"))\n            }\n\n            OutlinedButton(\n                onClick = {\n                    urlText = \"https://\"\n                    fullPage = true\n                    waitUntil = \"networkidle\"\n                    timeoutText = \"30000\"\n                    viewportWidthText = \"\"\n                    viewportHeightText = \"\"\n                    clarityScaleText = \"2.0\"\n                },\n                modifier = Modifier.weight(1f)\n            ) {\n                Text(i18nState.getString(\"reset\"))\n            }\n        }\n\n        // 使用说明\n        Card(\n            modifier = Modifier.fillMaxWidth(),\n            backgroundColor = MaterialTheme.colors.surface.copy(alpha = 0.5f),\n            shape = RoundedCornerShape(8.dp)\n        ) {\n            Column(\n                modifier = Modifier.padding(12.dp)\n            ) {\n                Text(\n                    text = i18nState.getString(\"usage_tips\"),\n                    style = MaterialTheme.typography.caption,\n                    color = MaterialTheme.colors.onSurface.copy(alpha = 0.7f)\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/controlpanel/webscreenshot/WebScreenshotViewModel.kt",
    "content": "package cn.netdiscovery.monica.ui.controlpanel.webscreenshot\n\nimport cn.netdiscovery.monica.state.ApplicationState\nimport cn.netdiscovery.monica.utils.WebScreenshotOptions\nimport cn.netdiscovery.monica.utils.checkNodeInstalled\nimport cn.netdiscovery.monica.utils.loadWebScreenshotToState\nimport org.slf4j.Logger\nimport org.slf4j.LoggerFactory\n\n/**\n * 网页截图 ViewModel\n * \n * @author: Tony Shen\n * @date: 2026/01/12\n * @version: V1.0\n */\nclass WebScreenshotViewModel {\n    \n    private val logger: Logger = LoggerFactory.getLogger(WebScreenshotViewModel::class.java)\n\n    /**\n     * 捕获网页截图\n     */\n    fun captureWebScreenshot(\n        state: ApplicationState,\n        url: String,\n        options: WebScreenshotOptions = WebScreenshotOptions()\n    ) {\n        logger.info(\"开始捕获网页截图: $url\")\n        loadWebScreenshotToState(state, url, options)\n    }\n\n    /**\n     * 检查 Node.js 环境\n     */\n    fun checkEnvironment(): Boolean {\n        return checkNodeInstalled()\n    }\n}\n"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/i18n/ComposeI18n.kt",
    "content": "package cn.netdiscovery.monica.ui.i18n\n\nimport androidx.compose.runtime.*\nimport cn.netdiscovery.monica.i18n.LocalizationManager\nimport cn.netdiscovery.monica.i18n.Language\n\n/**\n * Compose专用的国际化状态管理\n * 用于在UI中响应语言变化\n */\n@Composable\nfun rememberI18nState(): I18nState {\n    // 创建响应式的语言状态\n    var currentLanguage by remember { mutableStateOf(LocalizationManager.currentLanguage) }\n    \n    // 监听语言变化\n    LaunchedEffect(Unit) {\n        LocalizationManager.addLanguageChangeListener {\n            // 当语言变化时，更新Compose状态\n            currentLanguage = LocalizationManager.currentLanguage\n        }\n    }\n    \n    return remember(currentLanguage) {\n        I18nState(currentLanguage)\n    }\n}\n\n/**\n * 国际化状态类\n */\nclass I18nState(private val language: Language) {\n    \n    /**\n     * 获取当前语言的字符串资源\n     */\n    fun getString(key: String): String {\n        return LocalizationManager.getString(key)\n    }\n    \n    /**\n     * 获取带参数的字符串资源\n     */\n    fun getString(key: String, vararg args: Any): String {\n        return LocalizationManager.getString(key, *args)\n    }\n    \n    /**\n     * 获取当前语言\n     */\n    fun getCurrentLanguage(): Language = language\n    \n    /**\n     * 获取语言显示名称\n     */\n    fun getLanguageDisplayName(): String {\n        return \"${language.flag} ${language.displayName}\"\n    }\n    \n    /**\n     * 切换语言\n     */\n    fun toggleLanguage() {\n        val newLang = if (language == Language.CHINESE) Language.ENGLISH else Language.CHINESE\n        LocalizationManager.setLanguage(newLang)\n    }\n    \n    /**\n     * 设置特定语言\n     */\n    fun setLanguage(newLanguage: Language) {\n        LocalizationManager.setLanguage(newLanguage)\n    }\n    \n    /**\n     * 重置为系统语言\n     */\n    fun resetToSystemLanguage() {\n        val systemLang = Language.getSystemLanguage()\n        LocalizationManager.setLanguage(systemLang)\n    }\n    \n    /**\n     * 获取切换按钮文本\n     */\n    fun getToggleButtonText(): String {\n        return if (language == Language.CHINESE) \"切换到英文\" else \"Switch to Chinese\"\n    }\n}\n\n/**\n * 便捷的字符串获取函数\n */\n@Composable\nfun getString(key: String): String {\n    val i18nState = rememberI18nState()\n    return i18nState.getString(key)\n}\n\n/**\n * 便捷的带参数字符串获取函数\n */\n@Composable\nfun getString(key: String, vararg args: Any): String {\n    val i18nState = rememberI18nState()\n    return i18nState.getString(key, *args)\n}\n"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/main/ContentPanel.kt",
    "content": "package cn.netdiscovery.monica.ui.main\n\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.Card\nimport androidx.compose.material.MaterialTheme\nimport androidx.compose.material.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\nimport cn.netdiscovery.monica.state.ApplicationState\nimport cn.netdiscovery.monica.ui.controlpanel.*\nimport cn.netdiscovery.monica.ui.controlpanel.ai.aiView\nimport cn.netdiscovery.monica.ui.i18n.rememberI18nState\n\n/**\n * 内容面板组件 - 显示选中模块的详细功能\n * @author: Tony Shen\n * @date: 2025/9/8\n * @version: V1.0\n */\n@Composable\nfun ContentPanel(\n    state: ApplicationState,\n    modifier: Modifier = Modifier\n) {\n    val i18nState = rememberI18nState()\n    \n    Card(\n        modifier = modifier\n            .fillMaxHeight()\n            .width(320.dp),\n        shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp, bottomEnd = 16.dp, bottomStart = 16.dp),\n        elevation = 8.dp,\n        backgroundColor = MaterialTheme.colors.surface\n    ) {\n        Column(\n            modifier = Modifier\n                .fillMaxHeight()\n                .padding(20.dp),\n            verticalArrangement = Arrangement.spacedBy(16.dp)\n        ) {\n            when {\n                state.isBasic -> {\n                    Text(\n                        text = i18nState.getString(\"basic_functions\"),\n                        style = MaterialTheme.typography.h6,\n                        color = MaterialTheme.colors.primary\n                    )\n                    basicView(state)\n                }\n                \n                state.isAI -> {\n                    Text(\n                        text = i18nState.getString(\"ai_laboratory\"),\n                        style = MaterialTheme.typography.h6,\n                        color = MaterialTheme.colors.primary\n                    )\n                    aiView(state)\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/main/Dialogs.kt",
    "content": "package cn.netdiscovery.monica.ui.main\n\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.material.*\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\n\nimport cn.netdiscovery.monica.config.*\nimport cn.netdiscovery.monica.ui.i18n.rememberI18nState\nimport cn.netdiscovery.monica.utils.Action\nimport picUrl\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.main.Dialogs\n * @author: Tony Shen\n * @date: 2025/4/17 11:57\n * @version: V1.0 <描述当前版本功能>\n */\n\n/**\n * 加载网络图片的对话框\n */\n@Composable\nfun openURLDialog(onConfirm: Action, onDismiss: Action) {\n    val i18nState = rememberI18nState()\n    \n    AlertDialog(\n        modifier = Modifier.width(600.dp).height(200.dp),\n        onDismissRequest = onDismiss,\n        title = {\n            Text(text = i18nState.getString(\"load_network_image_dialog\"))\n        },\n        text = {\n            Column(\n                verticalArrangement = Arrangement.Center\n            ) {\n                TextField(\n                    modifier = Modifier.fillMaxWidth(),\n                    value = picUrl,\n                    onValueChange = { picUrl = it }\n                )\n            }\n        },\n        confirmButton = {\n            TextButton(\n                onClick = {\n                    onConfirm.invoke()\n                }\n            ) {\n                Text(i18nState.getString(\"confirm\"))\n            }\n        },\n        dismissButton = {\n            TextButton(\n                onClick = {\n                    onDismiss.invoke()\n                }\n            ) {\n                Text(i18nState.getString(\"cancel\"))\n            }\n        }\n    )\n}\n\n@Composable\nfun showVersionInfo(onClick: Action) {\n    val i18nState = rememberI18nState()\n    \n    AlertDialog(onDismissRequest = {},\n        title = {\n            Text(i18nState.getString(\"monica_software_info\"))\n        },\n        text = {\n            Column {\n                val versionInfo = if (isProVersion) i18nState.getString(\"pro_version\") else i18nState.getString(\"test_version\")\n                Text(i18nState.getString(\"monica_version_info\", appVersion, versionInfo, buildTime))\n                Text(\"OS: $os, $osVersion, $arch\")\n                Text(\"JDK: $javaVersion, $javaVendor\")\n                Text(\"Kotlin: $kotlinVersion, Compose Desktop: $composeVersion\")\n                Text(i18nState.getString(\"opencv_version_info\", openCVVersion, imageProcessVersion))\n                Text(i18nState.getString(\"copyright_info\"))\n                Text(\"Wechat: fengzhizi715\")\n                Text(i18nState.getString(\"github_url\"))\n            }\n        },\n        confirmButton = {\n            Button(onClick = {\n                onClick.invoke()\n            }) {\n                Text(i18nState.getString(\"close\"))\n            }\n        })\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/main/GeneralSettingsDialog.kt",
    "content": "package cn.netdiscovery.monica.ui.main\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material.AlertDialog\nimport androidx.compose.material.Button\nimport androidx.compose.material.ButtonDefaults\nimport androidx.compose.material.Card\nimport androidx.compose.material.Checkbox\nimport androidx.compose.material.MaterialTheme\nimport androidx.compose.material.Tab\nimport androidx.compose.material.TabRow\nimport androidx.compose.material.TabRowDefaults\nimport androidx.compose.material.TabRowDefaults.tabIndicatorOffset\nimport androidx.compose.material.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport cn.netdiscovery.monica.config.STATUS_HTTP_SERVER_FAILED\nimport cn.netdiscovery.monica.config.STATUS_HTTP_SERVER_OK\nimport cn.netdiscovery.monica.exception.ErrorSeverity\nimport cn.netdiscovery.monica.exception.ErrorType\nimport cn.netdiscovery.monica.exception.showError\nimport cn.netdiscovery.monica.http.healthCheck\nimport cn.netdiscovery.monica.i18n.Language\nimport cn.netdiscovery.monica.i18n.LocalizationManager\nimport cn.netdiscovery.monica.rxcache.clearData\nimport cn.netdiscovery.monica.rxcache.initFilterParamsConfig\nimport cn.netdiscovery.monica.state.ApplicationState\nimport cn.netdiscovery.monica.ui.i18n.rememberI18nState\nimport cn.netdiscovery.monica.ui.theme.ColorTheme\nimport cn.netdiscovery.monica.ui.widget.basicTextFieldWithTitle\nimport cn.netdiscovery.monica.ui.widget.desktopLazyRow\nimport cn.netdiscovery.monica.utils.Action\nimport cn.netdiscovery.monica.utils.extensions.isValidUrl\nimport cn.netdiscovery.monica.utils.getValidateField\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.main.GeneralSettingsDialog\n * @author: Tony Shen\n * @date: 2025/9/9 18:09\n * @version: V1.0 <描述当前版本功能>\n */\n\n@Composable\nfun generalSettings(state: ApplicationState, onClick: Action) {\n    val i18nState = rememberI18nState()\n\n    var rText by remember { mutableStateOf(state.outputBoxRText.toString()) }\n    var gText by remember { mutableStateOf(state.outputBoxGText.toString()) }\n    var bText by remember { mutableStateOf(state.outputBoxBText.toString()) }\n    var sizeText by remember { mutableStateOf(state.sizeText.toString()) }\n    var maxHistorySizeText by remember { mutableStateOf(state.maxHistorySizeText.toString()) }\n    var deepSeekApiKeyText by remember { mutableStateOf(state.deepSeekApiKeyText) }\n    var geminiApiKeyText by remember { mutableStateOf(state.geminiApiKeyText) }\n    var algorithmUrlText by remember { mutableStateOf(state.algorithmUrlText) }\n    var isInitFilterParams by mutableStateOf(false)\n    var isClearCacheData by mutableStateOf(false)\n    var selectedTab by remember { mutableStateOf(0) }\n\n    var isServerOK by mutableStateOf(-1)\n\n    val tabTitles = listOf(\n        i18nState.getString(\"basic_settings\"),\n        i18nState.getString(\"api_settings\"),\n        i18nState.getString(\"theme_settings\"),\n        i18nState.getString(\"language_settings\")\n    )\n\n    AlertDialog(\n        onDismissRequest = {},\n        modifier = Modifier\n            .width(1000.dp)\n            .height(800.dp)\n            .background(MaterialTheme.colors.surface, RoundedCornerShape(16.dp)),\n        text = {\n            Box(\n                modifier = Modifier.fillMaxSize()\n            ) {\n                // 主要内容区域 - 使用TabRow进行分组\n                Column(\n                    modifier = Modifier.fillMaxSize()\n                ) {\n                    // 标题\n                    Text(\n                        text = i18nState.getString(\"monica_general_settings\"),\n                        fontSize = 20.sp,\n                        fontWeight = FontWeight.Bold,\n                        color = state.getCurrentThemeValue().primary,\n                        modifier = Modifier\n                    )\n\n                    // 标签页选择器\n                    TabRow(\n                        selectedTabIndex = selectedTab,\n                        modifier = Modifier.fillMaxWidth(),\n                        backgroundColor = MaterialTheme.colors.surface,\n                        contentColor = MaterialTheme.colors.onSurface,\n                        indicator = { tabPositions ->\n                            TabRowDefaults.Indicator(\n                                modifier = Modifier.tabIndicatorOffset(tabPositions[selectedTab]),\n                                height = 3.dp,\n                                color = state.getCurrentThemeValue().primary\n                            )\n                        }\n                    ) {\n                        tabTitles.forEachIndexed { index, title ->\n                            Tab(\n                                selected = selectedTab == index,\n                                onClick = { selectedTab = index },\n                                modifier = Modifier.padding(vertical = 12.dp),\n                                text = {\n                                    Text(\n                                        text = title,\n                                        fontSize = 14.sp,\n                                        fontWeight = if (selectedTab == index) FontWeight.Bold else FontWeight.Normal,\n                                        color = if (selectedTab == index) {\n                                            state.getCurrentThemeValue().primary\n                                        } else {\n                                            MaterialTheme.colors.onSurface.copy(alpha = 0.7f)\n                                        }\n                                    )\n                                }\n                            )\n                        }\n                    }\n\n                    // 标签页内容\n                    Box(modifier = Modifier.fillMaxSize().padding(top = 16.dp)) {\n                        when (selectedTab) {\n                            0 -> {\n                                // 基础设置\n                                Column(\n                                    modifier = Modifier\n                                        .fillMaxSize()\n                                        .verticalScroll(rememberScrollState())\n                                        .padding(16.dp),\n                                    verticalArrangement = Arrangement.spacedBy(16.dp)\n                                ) {\n                                    // 输出框颜色设置\n                                    Card(\n                                        modifier = Modifier.fillMaxWidth(),\n                                        elevation = 2.dp,\n                                        shape = RoundedCornerShape(12.dp),\n                                        backgroundColor = MaterialTheme.colors.surface\n                                    ) {\n                                        Column(\n                                            modifier = Modifier.padding(20.dp),\n                                            verticalArrangement = Arrangement.spacedBy(16.dp)\n                                        ) {\n                                            Text(\n                                                text = i18nState.getString(\"output_box_color_settings\"),\n                                                fontSize = 16.sp,\n                                                fontWeight = FontWeight.Bold,\n                                                color = MaterialTheme.colors.onSurface\n                                            )\n\n                                            Column(\n                                                verticalArrangement = Arrangement.spacedBy(12.dp)\n                                            ) {\n                                                basicTextFieldWithTitle(\n                                                    titleText = \"R\",\n                                                    value = rText,\n                                                    onValueChange = { rText = it },\n                                                    modifier = Modifier.fillMaxWidth()\n                                                )\n                                                basicTextFieldWithTitle(\n                                                    titleText = \"G\",\n                                                    value = gText,\n                                                    onValueChange = { gText = it },\n                                                    modifier = Modifier.fillMaxWidth()\n                                                )\n                                                basicTextFieldWithTitle(\n                                                    titleText = \"B\",\n                                                    value = bText,\n                                                    onValueChange = { bText = it },\n                                                    modifier = Modifier.fillMaxWidth()\n                                                )\n                                            }\n                                        }\n                                    }\n\n                                    // 区域大小设置\n                                    Card(\n                                        modifier = Modifier.fillMaxWidth(),\n                                        elevation = 2.dp,\n                                        shape = RoundedCornerShape(12.dp),\n                                        backgroundColor = MaterialTheme.colors.surface\n                                    ) {\n                                        Column(\n                                            modifier = Modifier.padding(20.dp),\n                                            verticalArrangement = Arrangement.spacedBy(16.dp)\n                                        ) {\n                                            Text(\n                                                text = i18nState.getString(\"area_size_settings\"),\n                                                fontSize = 16.sp,\n                                                fontWeight = FontWeight.Bold,\n                                                color = MaterialTheme.colors.onSurface\n                                            )\n\n                                            basicTextFieldWithTitle(\n                                                titleText = \"Size\",\n                                                value = sizeText,\n                                                onValueChange = { sizeText = it },\n                                                modifier = Modifier.fillMaxWidth()\n                                            )\n                                        }\n                                    }\n\n                                    // 历史记录大小设置\n                                    Card(\n                                        modifier = Modifier.fillMaxWidth(),\n                                        elevation = 2.dp,\n                                        shape = RoundedCornerShape(12.dp),\n                                        backgroundColor = MaterialTheme.colors.surface\n                                    ) {\n                                        Column(\n                                            modifier = Modifier.padding(20.dp),\n                                            verticalArrangement = Arrangement.spacedBy(16.dp)\n                                        ) {\n                                            Text(\n                                                text = i18nState.getString(\"max_history_size\"),\n                                                fontSize = 16.sp,\n                                                fontWeight = FontWeight.Bold,\n                                                color = MaterialTheme.colors.onSurface\n                                            )\n\n                                            basicTextFieldWithTitle(\n                                                titleText = \"Max History Size\",\n                                                value = maxHistorySizeText,\n                                                onValueChange = { maxHistorySizeText = it },\n                                                modifier = Modifier.fillMaxWidth()\n                                            )\n                                        }\n                                    }\n\n                                    // 选项设置\n                                    Card(\n                                        modifier = Modifier.fillMaxWidth(),\n                                        elevation = 2.dp,\n                                        shape = RoundedCornerShape(12.dp),\n                                        backgroundColor = MaterialTheme.colors.surface\n                                    ) {\n                                        Column(\n                                            modifier = Modifier.padding(12.dp),\n                                        ) {\n                                            Text(\n                                                text = i18nState.getString(\"options_settings\"),\n                                                fontSize = 16.sp,\n                                                fontWeight = FontWeight.Bold,\n                                                color = MaterialTheme.colors.onSurface\n                                            )\n\n                                            Row(\n                                                verticalAlignment = Alignment.CenterVertically,\n                                                horizontalArrangement = Arrangement.spacedBy(16.dp)\n                                            ) {\n                                                Checkbox(\n                                                    checked = isInitFilterParams,\n                                                    onCheckedChange = { isInitFilterParams = it }\n                                                )\n                                                Text(i18nState.getString(\"init_filter_params_config\"))\n                                            }\n\n                                            Row(\n                                                verticalAlignment = Alignment.CenterVertically,\n                                                horizontalArrangement = Arrangement.spacedBy(16.dp)\n                                            ) {\n                                                Checkbox(\n                                                    checked = isClearCacheData,\n                                                    onCheckedChange = { isClearCacheData = it }\n                                                )\n                                                Text(i18nState.getString(\"clear_cache_data\"))\n                                            }\n                                        }\n                                    }\n                                }\n                            }\n                            1 -> {\n                                // API设置\n                                Column(\n                                    modifier = Modifier\n                                        .fillMaxSize()\n                                        .verticalScroll(rememberScrollState())\n                                        .padding(16.dp),\n                                    verticalArrangement = Arrangement.spacedBy(16.dp)\n                                ) {\n                                    // DeepSeek API设置\n                                    Card(\n                                        modifier = Modifier.fillMaxWidth(),\n                                        elevation = 2.dp,\n                                        shape = RoundedCornerShape(12.dp),\n                                        backgroundColor = MaterialTheme.colors.surface\n                                    ) {\n                                        Column(\n                                            modifier = Modifier.padding(20.dp),\n                                            verticalArrangement = Arrangement.spacedBy(16.dp)\n                                        ) {\n                                            Text(\n                                                text = i18nState.getString(\"ai_provider_deepseek\") + \" API Key\",\n                                                fontSize = 16.sp,\n                                                fontWeight = FontWeight.Bold,\n                                                color = MaterialTheme.colors.onSurface\n                                            )\n\n                                            basicTextFieldWithTitle(\n                                                titleText = \"DeepSeek API Key\",\n                                                value = deepSeekApiKeyText,\n                                                onValueChange = { deepSeekApiKeyText = it },\n                                                modifier = Modifier.fillMaxWidth()\n                                            )\n                                        }\n                                    }\n\n                                    // Gemini API设置\n                                    Card(\n                                        modifier = Modifier.fillMaxWidth(),\n                                        elevation = 2.dp,\n                                        shape = RoundedCornerShape(12.dp),\n                                        backgroundColor = MaterialTheme.colors.surface\n                                    ) {\n                                        Column(\n                                            modifier = Modifier.padding(20.dp),\n                                            verticalArrangement = Arrangement.spacedBy(16.dp)\n                                        ) {\n                                            Text(\n                                                text = i18nState.getString(\"ai_provider_gemini\") + \" API Key\",\n                                                fontSize = 16.sp,\n                                                fontWeight = FontWeight.Bold,\n                                                color = MaterialTheme.colors.onSurface\n                                            )\n\n                                            basicTextFieldWithTitle(\n                                                titleText = \"Gemini API Key\",\n                                                value = geminiApiKeyText,\n                                                onValueChange = { geminiApiKeyText = it },\n                                                modifier = Modifier.fillMaxWidth()\n                                            )\n                                        }\n                                    }\n\n                                    // 算法URL设置\n                                    Card(\n                                        modifier = Modifier.fillMaxWidth(),\n                                        elevation = 2.dp,\n                                        shape = RoundedCornerShape(12.dp),\n                                        backgroundColor = MaterialTheme.colors.surface\n                                    ) {\n                                        Column(\n                                            modifier = Modifier.padding(20.dp),\n                                            verticalArrangement = Arrangement.spacedBy(16.dp)\n                                        ) {\n                                            Text(\n                                                text = i18nState.getString(\"algorithm_service_url\"),\n                                                fontSize = 16.sp,\n                                                fontWeight = FontWeight.Bold,\n                                                color = MaterialTheme.colors.onSurface\n                                            )\n\n                                            basicTextFieldWithTitle(\n                                                titleText = \"Algorithm URL\",\n                                                value = algorithmUrlText,\n                                                onValueChange = { algorithmUrlText = it },\n                                                modifier = Modifier.fillMaxWidth()\n                                            )\n\n                                            Row(verticalAlignment = Alignment.CenterVertically) {\n                                                Text(\n                                                    text = i18nState.getString(\"enter_complete_algorithm_url\"),\n                                                    fontSize = 12.sp,\n                                                    color = MaterialTheme.colors.onSurface.copy(alpha = 0.7f)\n                                                )\n\n                                                Button(\n                                                    onClick = {\n                                                        val status = try {\n                                                            val baseUrl = algorithmUrlText\n                                                            if (healthCheck(baseUrl)) {\n                                                                STATUS_HTTP_SERVER_OK\n                                                            } else {\n                                                                STATUS_HTTP_SERVER_FAILED\n                                                            }\n                                                        } catch (e:Exception) {\n                                                            STATUS_HTTP_SERVER_FAILED\n                                                        }\n\n                                                        isServerOK = if (status == STATUS_HTTP_SERVER_OK) {\n                                                            1\n                                                        } else {\n                                                            0\n                                                        }\n                                                    },\n                                                    enabled = algorithmUrlText.isNotEmpty(),\n                                                    modifier = Modifier.padding(start = 10.dp),\n                                                    colors = ButtonDefaults.buttonColors(backgroundColor = state.getCurrentThemeValue().primary)\n                                                ) {\n                                                    Text(i18nState.getString(\"is_the_algorithm_service_available\"), color = Color.White)\n                                                }\n\n                                                if (isServerOK == 1) {\n                                                    Text(i18nState.getString(\"algorithm_service_available\"),\n                                                        modifier = Modifier.padding(start = 10.dp),\n                                                        fontSize = 12.sp,\n                                                        color = MaterialTheme.colors.onSurface.copy(alpha = 0.7f))\n                                                } else if (isServerOK == 0) {\n                                                    Text(i18nState.getString(\"algorithm_service_unavailable\"),\n                                                        modifier = Modifier.padding(start = 10.dp),\n                                                        fontSize = 12.sp,\n                                                        color = MaterialTheme.colors.onSurface.copy(alpha = 0.7f))\n                                                }\n                                            }\n                                        }\n                                    }\n                                }\n                            }\n                            2 -> {\n                                // 主题设置\n                                Column(\n                                    modifier = Modifier\n                                        .fillMaxSize()\n                                        .padding(16.dp),\n                                    verticalArrangement = Arrangement.spacedBy(16.dp)\n                                ) {\n                                    // 当前主题显示\n                                    Card(\n                                        modifier = Modifier.fillMaxWidth(),\n                                        elevation = 2.dp,\n                                        shape = RoundedCornerShape(12.dp),\n                                        backgroundColor = MaterialTheme.colors.surface\n                                    ) {\n                                        Row(\n                                            modifier = Modifier.padding(20.dp),\n                                        ) {\n                                            Text(\n                                                text = i18nState.getString(\"current_theme\"),\n                                                fontSize = 16.sp,\n                                                fontWeight = FontWeight.Bold,\n                                                color = MaterialTheme.colors.onSurface\n                                            )\n\n                                            Text(\n                                                text = state.getCurrentThemeValue().getThemeDisplayName(),\n                                                color = state.getCurrentThemeValue().primary,\n                                                fontSize = 16.sp,\n                                                fontWeight = FontWeight.Medium,\n                                                modifier = Modifier.padding(start = 20.dp)\n                                            )\n                                        }\n                                    }\n\n                                    // 主题选择\n                                    Card(\n                                        modifier = Modifier.fillMaxWidth(),\n                                        elevation = 2.dp,\n                                        shape = RoundedCornerShape(12.dp),\n                                        backgroundColor = MaterialTheme.colors.surface\n                                    ) {\n                                        Column(\n                                            modifier = Modifier.padding(20.dp),\n                                            verticalArrangement = Arrangement.spacedBy(16.dp)\n                                        ) {\n                                            Text(\n                                                text = i18nState.getString(\"select_theme\"),\n                                                fontSize = 16.sp,\n                                                fontWeight = FontWeight.Bold,\n                                                color = MaterialTheme.colors.onSurface\n                                            )\n\n                                            desktopLazyRow(modifier = Modifier.fillMaxWidth()) {\n                                                Row(\n                                                    horizontalArrangement = Arrangement.spacedBy(12.dp)\n                                                ) {\n                                                    ColorTheme.entries.forEach { theme ->\n                                                        val isSelected = state.getCurrentThemeValue() == theme\n                                                        Card(\n                                                            modifier = Modifier\n                                                                .width(120.dp)\n                                                                .height(80.dp)\n                                                                .clickable {\n                                                                    state.setTheme(theme)\n                                                                },\n                                                            elevation = if (isSelected) 8.dp else 2.dp,\n                                                            shape = RoundedCornerShape(8.dp),\n                                                            backgroundColor = theme.background\n                                                        ) {\n                                                            Box(\n                                                                modifier = Modifier.fillMaxSize(),\n                                                                contentAlignment = Alignment.Center\n                                                            ) {\n                                                                // 选中状态的边框效果\n                                                                if (isSelected) {\n                                                                    Box(\n                                                                        modifier = Modifier\n                                                                            .fillMaxSize()\n                                                                            .background(\n                                                                                Color.Transparent,\n                                                                                RoundedCornerShape(8.dp)\n                                                                            )\n                                                                            .border(\n                                                                                width = 2.dp,\n                                                                                color = theme.primary,\n                                                                                shape = RoundedCornerShape(8.dp)\n                                                                            )\n                                                                    )\n                                                                }\n\n                                                                Text(\n                                                                    text = theme.getThemeDisplayName(),\n                                                                    color = theme.onBackground,\n                                                                    fontSize = 12.sp,\n                                                                    fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Medium\n                                                                )\n                                                            }\n                                                        }\n                                                    }\n                                                }\n                                            }\n                                        }\n                                    }\n\n                                    // 重置按钮\n                                    Card(\n                                        modifier = Modifier.fillMaxWidth(),\n                                        elevation = 2.dp,\n                                        shape = RoundedCornerShape(12.dp),\n                                        backgroundColor = MaterialTheme.colors.surface\n                                    ) {\n                                        Column(\n                                            modifier = Modifier.padding(20.dp),\n                                            verticalArrangement = Arrangement.spacedBy(16.dp)\n                                        ) {\n                                            Text(\n                                                text = i18nState.getString(\"theme_operations\"),\n                                                fontSize = 16.sp,\n                                                fontWeight = FontWeight.Bold,\n                                                color = MaterialTheme.colors.onSurface\n                                            )\n\n                                            Button(\n                                                onClick = {\n                                                    state.setTheme(ColorTheme.LIGHT)\n                                                },\n                                                modifier = Modifier.fillMaxWidth(),\n                                                colors = ButtonDefaults.buttonColors(backgroundColor = state.getCurrentThemeValue().primary)\n                                            ) {\n                                                Text(i18nState.getString(\"reset_to_default_theme\"), color = Color.White)\n                                            }\n                                        }\n                                    }\n                                }\n                            }\n                            3 -> {\n                                // 语言设置\n                                Column(\n                                    modifier = Modifier\n                                        .fillMaxSize()\n                                        .padding(16.dp),\n                                    verticalArrangement = Arrangement.spacedBy(16.dp)\n                                ) {\n                                    // 当前语言显示\n                                    Card(\n                                        modifier = Modifier.fillMaxWidth(),\n                                        elevation = 2.dp,\n                                        shape = RoundedCornerShape(12.dp),\n                                        backgroundColor = MaterialTheme.colors.surface\n                                    ) {\n                                        Row (\n                                            modifier = Modifier.padding(20.dp)\n                                        ) {\n                                            Text(\n                                                text = i18nState.getString(\"current_language\"),\n                                                fontSize = 16.sp,\n                                                fontWeight = FontWeight.Bold,\n                                                color = MaterialTheme.colors.onSurface\n                                            )\n\n                                            Text(\n                                                text = if (LocalizationManager.currentLanguage == Language.CHINESE) i18nState.getString(\"chinese\") else i18nState.getString(\"english\"),\n                                                fontSize = 16.sp,\n                                                fontWeight = FontWeight.Medium,\n                                                color = MaterialTheme.colors.onSurface,\n                                                modifier = Modifier.padding(start = 20.dp)\n                                            )\n                                        }\n                                    }\n\n                                    // 语言切换\n                                    Card(\n                                        modifier = Modifier.fillMaxWidth(),\n                                        elevation = 2.dp,\n                                        shape = RoundedCornerShape(12.dp),\n                                        backgroundColor = MaterialTheme.colors.surface\n                                    ) {\n                                        Column(\n                                            modifier = Modifier.padding(20.dp),\n                                            verticalArrangement = Arrangement.spacedBy(16.dp)\n                                        ) {\n                                            Text(\n                                                text = i18nState.getString(\"language_switch\"),\n                                                fontSize = 16.sp,\n                                                fontWeight = FontWeight.Bold,\n                                                color = MaterialTheme.colors.onSurface\n                                            )\n\n                                            Row(\n                                                horizontalArrangement = Arrangement.spacedBy(12.dp)\n                                            ) {\n                                                Button(\n                                                    onClick = {\n                                                        LocalizationManager.setLanguage(Language.CHINESE)\n                                                    },\n                                                    modifier = Modifier.weight(1f),\n                                                    colors = ButtonDefaults.buttonColors(\n                                                        backgroundColor = if (LocalizationManager.currentLanguage == Language.CHINESE)\n                                                            state.getCurrentThemeValue().primary\n                                                        else\n                                                            MaterialTheme.colors.surface\n                                                    )\n                                                ) {\n                                                    Text(\n                                                        text = i18nState.getString(\"chinese\"),\n                                                        color = if (LocalizationManager.currentLanguage == Language.CHINESE)\n                                                            MaterialTheme.colors.onPrimary\n                                                        else\n                                                            MaterialTheme.colors.onSurface\n                                                    )\n                                                }\n\n                                                Button(\n                                                    onClick = {\n                                                        LocalizationManager.setLanguage(Language.ENGLISH)\n                                                    },\n                                                    modifier = Modifier.weight(1f),\n                                                    colors = ButtonDefaults.buttonColors(\n                                                        backgroundColor = if (LocalizationManager.currentLanguage == Language.ENGLISH)\n                                                            state.getCurrentThemeValue().primary\n                                                        else\n                                                            MaterialTheme.colors.surface\n                                                    )\n                                                ) {\n                                                    Text(\n                                                        text = \"English\",\n                                                        color = if (LocalizationManager.currentLanguage == Language.ENGLISH)\n                                                            MaterialTheme.colors.onPrimary\n                                                        else\n                                                            MaterialTheme.colors.onSurface\n                                                    )\n                                                }\n                                            }\n                                        }\n                                    }\n\n                                    // 重置语言\n                                    Card(\n                                        modifier = Modifier.fillMaxWidth(),\n                                        elevation = 2.dp,\n                                        shape = RoundedCornerShape(12.dp),\n                                        backgroundColor = MaterialTheme.colors.surface\n                                    ) {\n                                        Column(\n                                            modifier = Modifier.padding(20.dp),\n                                            verticalArrangement = Arrangement.spacedBy(16.dp)\n                                        ) {\n                                            Text(\n                                                text = i18nState.getString(\"language_operations\"),\n                                                fontSize = 16.sp,\n                                                fontWeight = FontWeight.Bold,\n                                                color = MaterialTheme.colors.onSurface\n                                            )\n\n                                            Button(\n                                                onClick = {\n                                                    LocalizationManager.setLanguage(Language.CHINESE)\n                                                },\n                                                modifier = Modifier.fillMaxWidth(),\n                                                colors = ButtonDefaults.buttonColors(backgroundColor = state.getCurrentThemeValue().primary)\n                                            ) {\n                                                Text(i18nState.getString(\"reset_to_chinese\"), color = Color.White)\n                                            }\n                                        }\n                                    }\n                                }\n                            }\n                        }\n                    }\n                }\n\n                // 底部按钮区域 - 固定在底部\n                Row(\n                    modifier = Modifier\n                        .align(Alignment.BottomCenter)\n                        .fillMaxWidth()\n                        .padding(16.dp)\n                        .background(\n                            MaterialTheme.colors.surface,\n                            RoundedCornerShape(8.dp)\n                        )\n                        .padding(12.dp),\n                    horizontalArrangement = Arrangement.spacedBy(12.dp),\n                    verticalAlignment = Alignment.CenterVertically\n                ) {\n                    Spacer(modifier = Modifier.weight(1f))\n\n                    Button(\n                        onClick = {\n                            state.outputBoxRText = getValidateField(block = { rText.toInt() }, failed = {\n                                    val errorMsg = i18nState.getString(\"r_needs_int\")\n                                    showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.LOW, errorMsg, errorMsg)\n                                }) ?: return@Button\n                            state.outputBoxGText = getValidateField(block = { gText.toInt() }, failed = {\n                                    val errorMsg = i18nState.getString(\"g_needs_int\")\n                                    showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.LOW, errorMsg, errorMsg)\n                                }) ?: return@Button\n                            state.outputBoxBText = getValidateField(block = { bText.toInt() }, failed = {\n                                val errorMsg = i18nState.getString(\"b_needs_int\")\n                                showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.LOW, errorMsg, errorMsg)\n                            }) ?: return@Button\n                            state.sizeText = getValidateField(block = { sizeText.toInt() }, failed = {\n                                val errorMsg = i18nState.getString(\"size_needs_int\")\n                                showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.LOW, errorMsg, errorMsg)\n                            }) ?: return@Button\n                            state.deepSeekApiKeyText = deepSeekApiKeyText\n                            state.geminiApiKeyText = geminiApiKeyText\n                            state.algorithmUrlText = if (algorithmUrlText.isNotEmpty()) {\n                                getValidateField(block = {\n                                    if (algorithmUrlText.isValidUrl()) {\n                                        if (algorithmUrlText.last() == '/') {\n                                            algorithmUrlText\n                                        } else {\n                                            \"$algorithmUrlText/\"\n                                        }\n                                    } else {\n                                        throw RuntimeException()\n                                    }\n                                }, failed = {\n                                    val errorMsg = i18nState.getString(\"enter_valid_url\")\n                                    showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.LOW, errorMsg, errorMsg)\n                                }) ?: return@Button\n                            } else \"\"\n\n                            state.maxHistorySizeText = getValidateField(block = { maxHistorySizeText.toInt() }, failed = {\n                                val errorMsg = i18nState.getString(\"max_history_size_needs_int\")\n                                showError(ErrorType.VALIDATION_ERROR, ErrorSeverity.LOW, errorMsg, errorMsg)\n                            }) ?: return@Button\n\n                            state.saveGeneralSettings()\n\n                            if (isInitFilterParams) {\n                                initFilterParamsConfig()\n                            }\n\n                            if (isClearCacheData) {\n                                clearData()\n                            }\n\n                            onClick()\n                        },\n                        colors = ButtonDefaults.buttonColors(backgroundColor = state.getCurrentThemeValue().primary)\n                    ) {\n                        Text(i18nState.getString(\"update\"), color = Color.White)\n                    }\n\n                    Button(\n                        onClick = { onClick() },\n                        colors = ButtonDefaults.buttonColors(backgroundColor = state.getCurrentThemeValue().primary)\n                    ) {\n                        Text(i18nState.getString(\"close\"), color = Color.White)\n                    }\n                }\n            }\n        },\n        confirmButton = {\n            // 空内容，按钮在text中处理\n        },\n        dismissButton = {\n            // 空内容，按钮在text中处理\n        }\n    )\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/main/MainView.kt",
    "content": "package cn.netdiscovery.monica.ui.main\n\nimport androidx.compose.animation.*\nimport androidx.compose.animation.core.*\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.material.MaterialTheme\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.unit.dp\nimport cn.netdiscovery.monica.state.ApplicationState\nimport cn.netdiscovery.monica.ui.preview.preview\nimport org.koin.compose.koinInject\n\n/**\n * 主页面视图 - 现代化布局\n * @author: Tony Shen\n * @date: 2025/9/8\n * @version: V1.0\n */\n@Composable\nfun mainView(\n    state: ApplicationState\n) {\n    val viewModel: MainViewModel = koinInject()\n\n    viewModel.dropFile(state)\n\n    Box(\n        modifier = Modifier\n            .fillMaxSize()\n            .background(\n                brush = Brush.verticalGradient(\n                    colors = listOf(\n                        MaterialTheme.colors.background,\n                        MaterialTheme.colors.surface\n                    )\n                )\n            )\n    ) {\n        Row(\n            modifier = Modifier\n                .fillMaxSize()\n                .padding(24.dp), // 增加整体边距\n            verticalAlignment = Alignment.CenterVertically,\n            horizontalArrangement = Arrangement.spacedBy(24.dp) // 增加组件间距\n        ) {\n            // 左侧菜单栏\n            SidebarView(state = state)\n\n            val hasSelectedItem by remember {\n                derivedStateOf {\n                    state.isGeneralSettings || state.isBasic ||\n                    state.isColorCorrection || state.isFilter || state.isAI\n                }\n            }\n\n            // 中间内容面板，根据是否有选中项显示\n            AnimatedVisibility(\n                visible = hasSelectedItem,\n                enter = slideInHorizontally(\n                    initialOffsetX = { -it },\n                    animationSpec = tween(300, easing = FastOutSlowInEasing)\n                ) + fadeIn(animationSpec = tween(300)),\n                exit = slideOutHorizontally(\n                    targetOffsetX = { -it },\n                    animationSpec = tween(300, easing = FastOutSlowInEasing)\n                ) + fadeOut(animationSpec = tween(300))\n            ) {\n                ContentPanel(state = state)\n            }\n\n            // 右侧预览区域\n            preview(state)\n        }\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/main/MainViewModel.kt",
    "content": "package cn.netdiscovery.monica.ui.main\n\nimport cn.netdiscovery.monica.state.ApplicationState\nimport cn.netdiscovery.monica.utils.dropFileTarget\nimport cn.netdiscovery.monica.utils.extensions.launchWithLoading\nimport cn.netdiscovery.monica.utils.getBufferedImage\nimport cn.netdiscovery.monica.utils.legalSuffixList\nimport java.io.File\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.main.MainViewModel\n * @author: Tony Shen\n * @date: 2024/5/24 11:03\n * @version: V1.0 <描述当前版本功能>\n */\nclass MainViewModel {\n\n    fun dropFile(state: ApplicationState) {\n        state.window.contentPane.dropTarget = dropFileTarget {\n            state.scope.launchWithLoading {\n                val filePath = it.getOrNull(0)\n                if (filePath != null) {\n                    val file = File(filePath)\n                    if (file.isFile && file.extension in legalSuffixList) {\n                        val image = getBufferedImage(file, state)\n                        state.rawImage = image\n                        state.currentImage = state.rawImage\n                        state.rawImageFile = file\n                    }\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/main/SidebarView.kt",
    "content": "package cn.netdiscovery.monica.ui.main\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.*\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport cn.netdiscovery.monica.config.appVersion\nimport cn.netdiscovery.monica.state.ApplicationState\nimport cn.netdiscovery.monica.state.ColorCorrectionStatus\nimport cn.netdiscovery.monica.state.FilterStatus\nimport cn.netdiscovery.monica.ui.i18n.rememberI18nState\nimport cn.netdiscovery.monica.ui.widget.rememberThrottledClick\nimport org.slf4j.Logger\nimport org.slf4j.LoggerFactory\nimport showGeneralSettings\n\n/**\n * 侧边栏组件 - NavigationRail 风格\n * @author: Tony Shen\n * @date: 2025/9/8\n * @version: V1.0\n */\nprivate val logger: Logger = LoggerFactory.getLogger(object : Any() {}.javaClass.enclosingClass)\n\nenum class SidebarItem(\n    val titleKey: String,\n    val iconPath: String,\n    val isEnabled: (ApplicationState) -> Boolean,\n    val onClick: (ApplicationState) -> Unit\n) {\n    BASIC_FUNCTIONS(\n        titleKey = \"basic_functions\",\n        iconPath = \"images/sidebar/basic_functions.png\",\n        isEnabled = { state -> state.isBasic },\n        onClick = { state ->\n            state.isBasic = !state.isBasic\n            // 切换基础功能时，如果关闭则同时关闭压缩子模块\n            if (!state.isBasic) {\n                state.isCompression = false\n            }\n\n            state.isGeneralSettings = false\n            state.isColorCorrection = false\n            state.isFilter = false\n            state.isAI = false\n        }\n    ),\n    COLOR_CORRECTION(\n        titleKey = \"image_color_correction\",\n        iconPath = \"images/sidebar/image_color_correction.png\",\n        isEnabled = { state -> state.isColorCorrection },\n        onClick = { state ->\n            state.togglePreviewWindowAndUpdateStatus(ColorCorrectionStatus)\n\n            state.isGeneralSettings = false\n            state.isBasic = false\n            state.isCompression = false\n            state.isFilter = false\n            state.isAI = false\n        }\n    ),\n    FILTER(\n        titleKey = \"filter_effects\",\n        iconPath = \"images/sidebar/filter_effects.png\",\n        isEnabled = { state -> state.isFilter },\n        onClick = { state ->\n            state.togglePreviewWindowAndUpdateStatus(FilterStatus)\n\n            state.isBasic = false\n            state.isCompression = false\n            state.isGeneralSettings = false\n            state.isColorCorrection = false\n            state.isAI = false\n        }\n    ),\n    AI_LAB(\n        titleKey = \"ai_laboratory\",\n        iconPath = \"images/sidebar/ai_laboratory.png\",\n        isEnabled = { state -> state.isAI },\n        onClick = { state ->\n            state.isAI = !state.isAI\n\n            state.isBasic = false\n            state.isCompression = false\n            state.isGeneralSettings = false\n            state.isColorCorrection = false\n            state.isFilter = false\n        }\n    ),\n    GENERAL_SETTINGS(\n        titleKey = \"general_settings\",\n        iconPath = \"images/sidebar/settings.png\",\n        isEnabled = { state -> state.isGeneralSettings },\n        onClick = { state ->\n            showGeneralSettings = true\n\n            state.isBasic = false\n            state.isCompression = false\n            state.isColorCorrection = false\n            state.isFilter = false\n            state.isAI = false\n        }\n    )\n}\n\n@Composable\nfun SidebarView(\n    state: ApplicationState,\n    modifier: Modifier = Modifier\n) {\n    val i18nState = rememberI18nState()\n    \n    Card(\n        modifier = modifier\n            .width(240.dp)\n            .fillMaxHeight(),\n        shape = RoundedCornerShape(16.dp),\n        elevation = 8.dp,\n        backgroundColor = MaterialTheme.colors.surface\n    ) {\n        Column(\n            modifier = Modifier\n                .fillMaxHeight()\n                .padding(16.dp),\n            verticalArrangement = Arrangement.SpaceBetween\n        ) {\n            Column(\n                verticalArrangement = Arrangement.spacedBy(8.dp)\n            ) {\n                // 标题\n                Text(\n                    text = i18nState.getString(\"app_name\"),\n                    fontSize = 20.sp,\n                    fontWeight = FontWeight.Bold,\n                    color = MaterialTheme.colors.primary,\n                    modifier = Modifier.padding(bottom = 16.dp)\n                )\n                \n                // 菜单项目\n                SidebarItem.entries.forEach { item ->\n                    SidebarMenuItem(\n                        item = item,\n                        state = state,\n                        i18nState = i18nState\n                    )\n                }\n            }\n\n            // 底部版本信息\n            Column(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .padding(vertical = 8.dp),\n                horizontalAlignment = Alignment.CenterHorizontally\n            ) {\n                Text(\n                    text = \"版本信息\",\n                    fontSize = 14.sp,\n                    color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f),\n                    fontWeight = FontWeight.Medium\n                )\n                Spacer(modifier = Modifier.height(4.dp))\n                Text(\n                    text = appVersion,\n                    fontSize = 14.sp,\n                    color = MaterialTheme.colors.primary,\n                    fontWeight = FontWeight.Bold\n                )\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun SidebarMenuItem(\n    item: SidebarItem,\n    state: ApplicationState,\n    i18nState: cn.netdiscovery.monica.ui.i18n.I18nState\n) {\n    val isSelected = item.isEnabled(state)\n    \n    Card(\n        modifier = Modifier\n            .fillMaxWidth()\n            .height(56.dp)\n            .clickable(\n                onClick = rememberThrottledClick {\n                    logger.info(\"点击了侧边栏项目: ${item.titleKey}\")\n                    item.onClick(state)\n                }\n            ),\n        shape = RoundedCornerShape(12.dp),\n        elevation = if (isSelected) 4.dp else 0.dp,\n        backgroundColor = if (isSelected) \n            MaterialTheme.colors.primary.copy(alpha = 0.1f) \n        else \n            Color.Transparent\n    ) {\n        Row(\n            modifier = Modifier\n                .fillMaxSize()\n                .padding(horizontal = 16.dp),\n            verticalAlignment = Alignment.CenterVertically,\n            horizontalArrangement = Arrangement.spacedBy(12.dp)\n        ) {\n            Icon(\n                painter = painterResource(item.iconPath),\n                contentDescription = null,\n                modifier = Modifier.size(24.dp),\n                tint = if (isSelected) \n                    MaterialTheme.colors.primary \n                else \n                    MaterialTheme.colors.onSurface.copy(alpha = 0.6f)\n            )\n            \n            Text(\n                text = i18nState.getString(item.titleKey),\n                fontSize = 14.sp,\n                fontWeight = if (isSelected) FontWeight.Medium else FontWeight.Normal,\n                color = if (isSelected) \n                    MaterialTheme.colors.primary \n                else \n                    MaterialTheme.colors.onSurface.copy(alpha = 0.8f)\n            )\n        }\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/preview/PreviewViewModel.kt",
    "content": "package cn.netdiscovery.monica.ui.preview\n\nimport androidx.compose.ui.geometry.Offset\nimport cn.netdiscovery.monica.config.KEY_GENERAL_SETTINGS\nimport cn.netdiscovery.monica.config.category.ConfigCategoryManager\nimport cn.netdiscovery.monica.domain.GeneralSettings\nimport cn.netdiscovery.monica.http.httpClient\nimport cn.netdiscovery.monica.imageprocess.BufferedImages\nimport cn.netdiscovery.monica.imageprocess.filter.blur.FastBlur2D\nimport cn.netdiscovery.monica.imageprocess.utils.extension.*\nimport cn.netdiscovery.monica.imageprocess.utils.writeImageFile\nimport cn.netdiscovery.monica.imageprocess.utils.writeImageFileAsWebP\nimport cn.netdiscovery.monica.manager.OpenCVManager\nimport cn.netdiscovery.monica.opencv.ImageProcess\nimport cn.netdiscovery.monica.state.ApplicationState\nimport cn.netdiscovery.monica.utils.ImageFormatDetector\nimport cn.netdiscovery.monica.i18n.LocalizationManager\nimport cn.netdiscovery.monica.utils.exportImage\nimport cn.netdiscovery.monica.utils.extensions.launchWithLoading\nimport cn.netdiscovery.monica.utils.extensions.launchWithSuspendLoading\nimport cn.netdiscovery.monica.utils.logger\nimport com.safframework.kotlin.coroutines.IO\nimport kotlinx.coroutines.launch\nimport org.slf4j.Logger\nimport showTopToast\nimport java.awt.Color\nimport java.awt.Graphics\nimport java.awt.image.BufferedImage\nimport java.io.File\nimport javax.imageio.ImageIO\nimport javax.swing.filechooser.FileNameExtensionFilter\n\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.preview.PreviewViewModel\n * @author: Tony Shen\n * @date: 2024/5/7 20:30\n * @version: V1.0 <描述当前版本功能>\n */\nclass PreviewViewModel {\n\n    private val logger: Logger = logger<PreviewViewModel>()\n\n    private val blurFilter = FastBlur2D(15)\n    \n    /**\n     * 获取 GeneralSettings.size，带默认值\n     */\n    private fun getGeneralSettingsSize(): Int {\n        val defaultSettings = GeneralSettings(255, 255, 255, 512, 50, \"\", \"\", \"\", \"LIGHT\")\n        val settings = ConfigCategoryManager.load(KEY_GENERAL_SETTINGS, defaultSettings)\n        return settings.size\n    }\n\n    fun loadUrl(picUrl:String, state: ApplicationState) {\n        logger.info(\"load picUrl: $picUrl\")\n\n        state.scope.launchWithSuspendLoading {\n            try {\n                val inputStream = httpClient.get(picUrl).body?.byteStream()\n                val bufferedImage = ImageIO.read(inputStream)\n\n                state.rawImage = bufferedImage\n                state.currentImage = state.rawImage\n            } catch (_: Exception) {\n            }\n        }\n    }\n\n    fun recoverImage(state: ApplicationState) {\n        state.currentImage = state.rawImage\n        state.clearQueue()\n    }\n\n    fun getLastImage(state: ApplicationState) {\n        state.getLastImage()?.let {\n            state.currentImage = it\n        }\n    }\n\n    fun blur(width:Int, height:Int,offset: Offset,state: ApplicationState) {\n\n        state.scope.launch(IO) {\n            val bufferedImage = state.currentImage!!\n\n            val srcWidth = bufferedImage.width\n            val srcHeight = bufferedImage.height\n\n            val xScale = (srcWidth.toFloat()/width)\n            val yScale = (srcHeight.toFloat()/height)\n\n            val size = getGeneralSettingsSize()\n\n            // 打码区域左上角x坐标\n            val x = (offset.x*xScale).toInt()\n            // 打码区域左上角y坐标\n            val y = (offset.y*yScale).toInt()\n            // 打码区域宽度\n            val width = (size*xScale).toInt()\n            // 打码区域高度\n            val height = (size*yScale).toInt()\n\n            var tempImage = bufferedImage.subImage(x,y,width,height)\n            tempImage = blurFilter.transform(tempImage)\n\n            val outputImage = BufferedImages.create(srcWidth, srcHeight, state.currentImage!!.type)\n            val graphics2D = outputImage.createGraphics()\n            graphics2D.drawImage(bufferedImage, 0, 0, null)\n            graphics2D.drawImage(tempImage, x, y, width, height, null)\n            graphics2D.dispose()\n\n            state.addQueue(state.currentImage!!)\n            state.currentImage = outputImage\n        }\n    }\n\n    fun mosaic(width:Int, height:Int,offset: Offset,state: ApplicationState) {\n\n        state.scope.launch(IO) {\n            val bufferedImage = state.currentImage!!\n\n            val srcWidth = bufferedImage.width\n            val srcHeight = bufferedImage.height\n\n            val xScale = (srcWidth.toFloat()/width)\n            val yScale = (srcHeight.toFloat()/height)\n\n            val size = getGeneralSettingsSize()\n\n            // 创建与输入图像相同大小的新图像\n            val outputImage = BufferedImages.create(srcWidth, srcHeight, state.currentImage!!.type)\n            // 创建画笔\n            val graphics: Graphics = outputImage.graphics\n            // 将原始图像绘制到新图像中\n            graphics.drawImage(bufferedImage, 0, 0, null)\n            // 打码区域左上角x坐标\n            val x = (offset.x*xScale).toInt()\n            // 打码区域左上角y坐标\n            val y = (offset.y*yScale).toInt()\n            // 打码区域宽度\n            val width = (size*xScale).toInt()\n            // 打码区域高度\n            val height = (size*yScale).toInt()\n\n            val mosaicSize = 40\n            var xcount = 0 // 方向绘制个数\n            var ycount = 0 // y方向绘制个数\n            xcount = if (width % mosaicSize === 0) {\n                width / mosaicSize\n            } else {\n                width / mosaicSize + 1\n            }\n\n            ycount = if (height % mosaicSize === 0) {\n                height / mosaicSize\n            } else {\n                height / mosaicSize + 1\n            }\n\n            var xTmp = x\n            var yTmp = y\n            for (i in 0 until xcount) {\n                for (j in 0 until ycount) {\n                    //马赛克矩形格大小\n                    var mwidth = mosaicSize\n                    var mheight = mosaicSize\n                    if (i == xcount - 1) {   //横向最后一个比较特殊，可能不够一个size\n                        mwidth = width - xTmp\n                    }\n                    if (j == ycount - 1) {  //同理\n                        mheight = height - yTmp\n                    }\n                    //矩形颜色取中心像素点RGB值\n                    var centerX = xTmp\n                    var centerY = yTmp\n                    centerX += if (mwidth % 2 == 0) {\n                        mwidth / 2\n                    } else {\n                        (mwidth - 1) / 2\n                    }\n                    centerY += if (mheight % 2 == 0) {\n                        mheight / 2\n                    } else {\n                        (mheight - 1) / 2\n                    }\n                    val color: Color = Color(bufferedImage.getRGB(centerX, centerY))\n                    graphics.setColor(color)\n                    graphics.fillRect(xTmp, yTmp, mwidth, mheight)\n                    yTmp += mosaicSize // 计算下一个矩形的y坐标\n                }\n                yTmp = y // 还原y坐标\n                xTmp += mosaicSize // 计算x坐标\n            }\n            // 释放资源\n            graphics.dispose()\n\n            state.addQueue(state.currentImage!!)\n            state.currentImage = outputImage\n        }\n    }\n\n    fun flip(state: ApplicationState) {\n\n        state.currentImage?.let {\n            state.addQueue(it)\n            state.currentImage = it.flipHorizontally()\n        }\n    }\n\n    fun rotate(state: ApplicationState) {\n\n        state.currentImage?.let {\n            state.addQueue(it)\n            state.currentImage = it.rotate(-90.0)\n        }\n    }\n\n    fun resize(width:Int, height:Int, state: ApplicationState) {\n\n        state.currentImage?.let {\n            if (width == it.width && height == it.height) {\n                return@let\n            }\n\n            val resizedImage = it.resize(width, height)\n            state.addQueue(it)\n            state.currentImage = resizedImage\n        }\n    }\n\n    fun shearing(x:Float, y:Float, state: ApplicationState) {\n\n        state.currentImage?.let {\n            if (x == 0f && y == 0f) {\n                return@let\n            }\n\n            state.scope.launchWithLoading {\n\n                OpenCVManager.invokeCV(state, action = { byteArray ->\n                    ImageProcess.shearing(byteArray, x, y)\n                }, failure = { e ->\n                    logger.error(\"shearing is failed\", e)\n                })\n            }\n        }\n    }\n\n    fun saveImage(state: ApplicationState) {\n\n        state.currentImage?.let {\n            exportImage { chooser ->\n                val selectedFile = chooser.selectedFile\n                val selectedFilter = chooser.fileFilter as FileNameExtensionFilter\n                val format = selectedFilter.extensions[0] // \"png\" or \"jpg\"\n\n                val outputFile = if (selectedFile.name.lowercase().endsWith(\".${format}\")) {\n                    selectedFile\n                } else {\n                    File(selectedFile.parent, \"${selectedFile.name}.${format}\")\n                }\n\n                val nativePtr = state.nativeImageInfo?.nativePtr\n\n                val nativeImage = if (nativePtr!=null && nativePtr!=0L) {\n                    ImageProcess.getNativeImage(nativePtr)\n                } else {\n                    null\n                }\n\n                val savedImage = if (nativeImage!=null) {\n                    BufferedImages.toBufferedImage(nativeImage.pixels, nativeImage.width, nativeImage.height, BufferedImage.TYPE_INT_ARGB)\n                } else {\n                    state.currentImage!!\n                }\n\n                val b = when(format) {\n                    \"jpg\" -> {\n                        val finalImage = if (state.rawImageFile==null) state.currentImage!!\n                        else {\n                            if (ImageFormatDetector.getImageFormat(state.rawImageFile!!) != \"jpeg\") {\n                                state.currentImage!!.convertToRGB()\n                            } else state.currentImage!!\n                        }\n\n                        writeImageFile(finalImage, outputFile.absolutePath, format)\n                    }\n\n                    \"png\" -> {\n                        writeImageFile(savedImage, outputFile.absolutePath, format)\n                    }\n\n                    \"webp\" -> {\n                        writeImageFileAsWebP(savedImage, outputFile.absolutePath)\n                    }\n\n                    else -> {\n                        writeImageFile(savedImage, outputFile.absolutePath, format)\n                    }\n                }\n\n                if (b)\n                    showTopToast(LocalizationManager.getString(\"image_save_success\"))\n                else\n                    showTopToast(LocalizationManager.getString(\"image_save_failed\"))\n            }\n        }\n    }\n\n    fun clearImage(state: ApplicationState) {\n        state.clearImage()\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/preview/PreviewViewt.kt",
    "content": "package cn.netdiscovery.monica.ui.preview\n\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.gestures.detectTapGestures\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.Card\nimport androidx.compose.material.ExperimentalMaterialApi\nimport androidx.compose.material.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.drawWithContent\nimport androidx.compose.ui.graphics.toPainter\nimport androidx.compose.ui.input.pointer.pointerInput\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.unit.dp\nimport cn.netdiscovery.monica.state.ApplicationState\nimport cn.netdiscovery.monica.state.BlurStatus\nimport cn.netdiscovery.monica.ui.i18n.rememberI18nState\nimport cn.netdiscovery.monica.state.MosaicStatus\nimport cn.netdiscovery.monica.state.ZoomPreviewStatus\nimport cn.netdiscovery.monica.ui.widget.toolTipButton\nimport cn.netdiscovery.monica.utils.chooseImage\nimport cn.netdiscovery.monica.utils.getBufferedImage\nimport org.koin.compose.koinInject\nimport org.slf4j.Logger\nimport org.slf4j.LoggerFactory\n\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.PreviewView\n * @author: Tony Shen\n * @date: 2024/4/26 11:09\n * @version: V1.0 <描述当前版本功能>\n */\nprivate val logger: Logger = LoggerFactory.getLogger(object : Any() {}.javaClass.enclosingClass)\n\n@OptIn(ExperimentalMaterialApi::class)\n@Composable\nfun preview(\n    state: ApplicationState,\n) {\n    val viewModel: PreviewViewModel = koinInject()\n\n    Card(\n        shape = RoundedCornerShape(16.dp),\n        elevation = 4.dp,\n        onClick = {\n            chooseImage(state) { file ->\n                val image = getBufferedImage(file, state)\n                state.rawImage = image\n                state.currentImage = state.rawImage\n                state.rawImageFile = file\n            }\n        },\n        enabled = state.rawImage == null\n    ) {\n        if (state.rawImage == null) {\n            chooseImage()\n        } else {\n            previewImage(state,viewModel)\n        }\n    }\n}\n\n@Composable\nprivate fun previewImage(state: ApplicationState, viewModel: PreviewViewModel) {\n    val i18nState = rememberI18nState()\n    if (state.currentImage == null) return\n\n    Column(\n        modifier = Modifier.fillMaxSize()\n    ) {\n        Column(\n            modifier = Modifier.fillMaxSize().weight(9f),\n            verticalArrangement = Arrangement.Center,\n            horizontalAlignment = Alignment.CenterHorizontally\n        ) {\n            Image(\n                painter = state.currentImage!!.toPainter(),\n                contentDescription = null,\n                contentScale = ContentScale.Fit,\n                modifier = Modifier\n                    .pointerInput(Unit) {\n\n                        val width = this.size.width\n                        val height = this.size.height\n\n                        detectTapGestures(\n                            onPress = {\n                                if (state.currentStatus == MosaicStatus) {\n                                    viewModel.mosaic(width, height, it, state)\n                                } else if (state.currentStatus == BlurStatus) {\n                                    viewModel.blur(width,height, it, state)\n                                }\n                            })\n                    }\n                    .drawWithContent {\n                    drawContent()\n                    })\n        }\n\n        Row (\n            modifier = Modifier.fillMaxSize().weight(1f),\n            horizontalArrangement = Arrangement.Center,\n            verticalAlignment = Alignment.CenterVertically\n        ) {\n            // 恢复最初\n            toolTipButton(text = i18nState.getString(\"restore_original\"),\n                painter = painterResource(\"images/preview/initial_picture.png\"),\n                iconModifier = Modifier.size(30.dp),\n                onClick = {\n                    viewModel.recoverImage(state)\n                })\n\n            // 上一步\n            toolTipButton(text = i18nState.getString(\"previous_step\"),\n                painter = painterResource(\"images/preview/reduction.png\"),\n                onClick = {\n                    viewModel.getLastImage(state)\n                })\n\n            // 放大预览\n            toolTipButton(text = i18nState.getString(\"enlarge_preview\"),\n                painter = painterResource(\"images/preview/zoom.png\"),\n                onClick = {\n                    state.togglePreviewWindowAndUpdateStatus(ZoomPreviewStatus)\n                })\n\n            // 保存\n            toolTipButton(text = i18nState.getString(\"save\"),\n                painter = painterResource(\"images/preview/save.png\"),\n                onClick = {\n                    viewModel.saveImage(state)\n                })\n\n            // 删除\n            toolTipButton(text = i18nState.getString(\"delete\"),\n                painter = painterResource(\"images/preview/delete.png\"),\n                onClick = {\n                    viewModel.clearImage(state)\n                })\n        }\n    }\n}\n\n@Composable\nprivate fun chooseImage() {\n    val i18nState = rememberI18nState()\n    Column(\n        modifier = Modifier.fillMaxSize(),\n        verticalArrangement = Arrangement.Center,\n        horizontalAlignment = Alignment.CenterHorizontally\n    ) {\n        Text(\n            text = i18nState.getString(\"click_to_select_image_or_drag\"),\n            textAlign = TextAlign.Center\n        )\n    }\n}\n"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/screenshot/SwingScreenshotAreaSelector.kt",
    "content": "package cn.netdiscovery.monica.ui.screenshot\n\nimport cn.netdiscovery.monica.state.ApplicationState\nimport cn.netdiscovery.monica.utils.captureRegion\nimport cn.netdiscovery.monica.utils.loadScreenshotToState\nimport org.slf4j.Logger\nimport org.slf4j.LoggerFactory\nimport java.awt.*\nimport java.awt.event.*\nimport javax.swing.JFrame\nimport javax.swing.JPanel\nimport javax.swing.SwingUtilities\n\n/**\n * 基于 Swing 的区域选择截图工具\n * 在 macOS 上更可靠地实现全屏透明窗口\n * \n * @author: Tony Shen\n * @date: 2025/12/03\n * @version: V1.0\n */\nprivate val logger: Logger = LoggerFactory.getLogger(object : Any() {}.javaClass.enclosingClass)\n\n/**\n * 显示 Swing 区域选择截图窗口\n */\nfun showSwingScreenshotAreaSelector(\n    state: ApplicationState,\n    onDismiss: () -> Unit\n) {\n    SwingUtilities.invokeLater {\n        ScreenshotAreaFrame(state, onDismiss)\n    }\n}\n\nprivate class ScreenshotAreaFrame(\n    private val state: ApplicationState,\n    private val onDismiss: () -> Unit\n) : JFrame() {\n    \n    private var startPoint: Point? = null\n    private var endPoint: Point? = null\n    private var isSelecting = false\n    \n    private val selectionPanel = object : JPanel() {\n        override fun paintComponent(g: Graphics) {\n            super.paintComponent(g)\n            \n            if (isSelecting && startPoint != null && endPoint != null) {\n                val g2d = g as Graphics2D\n                g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)\n                \n                val start = startPoint!!\n                val end = endPoint!!\n                \n                // 规范化坐标\n                val left = minOf(start.x, end.x)\n                val top = minOf(start.y, end.y)\n                val right = maxOf(start.x, end.x)\n                val bottom = maxOf(start.y, end.y)\n                val width = right - left\n                val height = bottom - top\n                \n                // 绘制半透明遮罩（选择区域外）\n                g2d.color = Color(0, 0, 0, 128) // 半透明黑色\n                \n                // 上方\n                if (top > 0) {\n                    g2d.fillRect(0, 0, size.width, top)\n                }\n                \n                // 下方\n                if (bottom < size.height) {\n                    g2d.fillRect(0, bottom, size.width, size.height - bottom)\n                }\n                \n                // 左侧\n                if (left > 0) {\n                    g2d.fillRect(0, top, left, height)\n                }\n                \n                // 右侧\n                if (right < size.width) {\n                    g2d.fillRect(right, top, size.width - right, height)\n                }\n                \n                // 绘制选择框边框\n                g2d.color = Color.WHITE\n                val oldStroke = g2d.stroke\n                g2d.stroke = BasicStroke(2f)\n                g2d.drawRect(left, top, width, height)\n                g2d.stroke = oldStroke\n                \n                // 绘制尺寸信息\n                val infoText = \"${width} × ${height}\"\n                g2d.color = Color.WHITE\n                g2d.font = Font(Font.SANS_SERIF, Font.PLAIN, 14)\n                val fontMetrics = g2d.fontMetrics\n                val textWidth = fontMetrics.stringWidth(infoText)\n                val textHeight = fontMetrics.height\n                \n                val textX = left + 5\n                val textY = if (top - textHeight - 5 > 0) {\n                    top - 5\n                } else {\n                    bottom + textHeight + 5\n                }\n                \n                // 绘制文本背景（提高可读性）\n                g2d.color = Color(0, 0, 0, 180)\n                g2d.fillRect(textX - 2, textY - textHeight - 2, textWidth + 4, textHeight + 4)\n                \n                // 绘制文本\n                g2d.color = Color.WHITE\n                g2d.drawString(infoText, textX, textY)\n            } else {\n                // 没有选择时，绘制全屏半透明遮罩\n                g.color = Color(0, 0, 0, 76) // 30% 透明度\n                g.fillRect(0, 0, size.width, size.height)\n            }\n        }\n    }\n    \n    init {\n        // 隐藏主窗口\n        logger.info(\"区域选择器打开，隐藏主窗口\")\n        state.window.isVisible = false\n        \n        // 设置窗口属性\n        isUndecorated = true\n        background = Color(0, 0, 0, 0) // 完全透明\n        isAlwaysOnTop = true\n        isResizable = false\n        \n        // 获取屏幕尺寸\n        val screenSize = Toolkit.getDefaultToolkit().screenSize\n        val graphicsConfig = GraphicsEnvironment.getLocalGraphicsEnvironment().defaultScreenDevice.defaultConfiguration\n        \n        // 设置窗口大小和位置\n        bounds = graphicsConfig.bounds\n        \n        // 设置内容面板\n        contentPane = selectionPanel\n        (contentPane as? JPanel)?.apply {\n            background = Color(0, 0, 0, 0)\n            isOpaque = false\n        }\n        \n        // 添加鼠标监听器\n        val mouseAdapter = object : MouseAdapter() {\n            override fun mousePressed(e: MouseEvent) {\n                startPoint = e.point\n                endPoint = e.point\n                isSelecting = true\n                selectionPanel.repaint()\n            }\n            \n            override fun mouseDragged(e: MouseEvent) {\n                endPoint = e.point\n                selectionPanel.repaint()\n            }\n            \n            override fun mouseReleased(e: MouseEvent) {\n                isSelecting = false\n                val start = startPoint\n                val end = endPoint\n                \n                if (start != null && end != null) {\n                    // 规范化坐标\n                    val x = minOf(start.x, end.x).coerceAtLeast(0)\n                    val y = minOf(start.y, end.y).coerceAtLeast(0)\n                    val width = (maxOf(start.x, end.x) - minOf(start.x, end.x)).coerceAtLeast(1)\n                    val height = (maxOf(start.y, end.y) - minOf(start.y, end.y)).coerceAtLeast(1)\n                    \n                    logger.info(\"选择区域: x=$x, y=$y, width=$width, height=$height\")\n                    \n                    // 在后台线程执行截图\n                    Thread {\n                        try {\n                            val screenshot = captureRegion(x, y, width, height)\n                            SwingUtilities.invokeLater {\n                                dispose() // 关闭窗口\n                                state.window.isVisible = true // 恢复主窗口\n                                \n                                if (screenshot != null) {\n                                    logger.info(\"截图成功，尺寸: ${screenshot.width}x${screenshot.height}\")\n                                    loadScreenshotToState(state, screenshot)\n                                } else {\n                                    logger.error(\"截图失败\")\n                                }\n                                onDismiss()\n                            }\n                        } catch (e: Exception) {\n                            logger.error(\"截图异常\", e)\n                            SwingUtilities.invokeLater {\n                                dispose()\n                                state.window.isVisible = true\n                                onDismiss()\n                            }\n                        }\n                    }.start()\n                } else {\n                    // 没有选择区域，直接关闭\n                    dispose()\n                    state.window.isVisible = true\n                    onDismiss()\n                }\n                \n                startPoint = null\n                endPoint = null\n            }\n        }\n        \n        selectionPanel.addMouseListener(mouseAdapter)\n        selectionPanel.addMouseMotionListener(mouseAdapter)\n        \n        // ESC 键关闭\n        val keyAdapter = object : KeyAdapter() {\n            override fun keyPressed(e: KeyEvent) {\n                if (e.keyCode == KeyEvent.VK_ESCAPE) {\n                    dispose()\n                    state.window.isVisible = true\n                    onDismiss()\n                }\n            }\n        }\n        addKeyListener(keyAdapter)\n        selectionPanel.isFocusable = true\n        selectionPanel.requestFocus()\n        \n        // 设置窗口关闭监听\n        defaultCloseOperation = JFrame.DISPOSE_ON_CLOSE\n        addWindowListener(object : WindowAdapter() {\n            override fun windowClosed(e: WindowEvent?) {\n                state.window.isVisible = true\n                onDismiss()\n            }\n        })\n        \n        // 显示窗口\n        isVisible = true\n        toFront()\n        selectionPanel.requestFocus()\n        \n        logger.info(\"Swing 区域选择窗口已显示，大小: ${bounds.width}x${bounds.height}\")\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/showimage/ShowImageView.kt",
    "content": "package cn.netdiscovery.monica.ui.showimage\n\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.gestures.detectTransformGestures\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.material.*\nimport androidx.compose.material.MaterialTheme\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.graphics.*\nimport androidx.compose.ui.input.pointer.pointerInput\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.layout.layout\nimport androidx.compose.ui.unit.Constraints\nimport androidx.compose.ui.unit.dp\nimport cn.netdiscovery.monica.state.ApplicationState\nimport cn.netdiscovery.monica.ui.i18n.rememberI18nState\nimport cn.netdiscovery.monica.utils.extensions.to2fStr\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.showimage.ShowImgView\n * @author: Tony Shen\n * @date: 2024/4/26 22:18\n * @version: V1.0 <描述当前版本功能>\n */\n@Composable\nfun showImage(\n    state: ApplicationState\n) {\n    val i18nState = rememberI18nState()\n    var angle   by remember { mutableStateOf(0f) }  // 旋转角度\n    var scale   by remember { mutableStateOf(1f) }  // 缩放\n    var offsetX by remember { mutableStateOf(0f) }  // x偏移\n    var offsetY by remember { mutableStateOf(0f) }  // y偏移\n    var matrix  by remember { mutableStateOf(Matrix()) }  // 矩阵\n\n    val image = state.currentImage!!.toComposeImageBitmap()\n\n    Box(\n        Modifier.fillMaxSize(),\n        contentAlignment = Alignment.Center\n    ) {\n        Image(\n            bitmap = image,\n            contentDescription = null,\n            contentScale = ContentScale.Fit,\n            modifier = Modifier\n                .fillMaxSize()\n                .graphicsLayer {\n                    scaleX = scale\n                    scaleY = scale\n                    rotationZ = angle\n                    translationX = offsetX\n                    translationY = offsetY\n                }\n                .pointerInput(Unit) {\n                    detectTransformGestures { centroid, pan, zoom, rotation ->\n                        angle += rotation\n                        scale *= zoom\n\n                        matrix.translate(pan.x, pan.y)\n                        matrix.rotateZ(rotation)\n                        matrix.scale(zoom, zoom)\n\n                        matrix = Matrix(matrix.values)\n\n                        offsetX = matrix.values[Matrix.TranslateX]\n                        offsetY = matrix.values[Matrix.TranslateY]\n                    }\n                }\n        )\n\n        Row (modifier = Modifier.align(Alignment.CenterEnd).padding(end = 10.dp)){\n\n            Column(\n                Modifier.padding(end = 10.dp),\n                verticalArrangement = Arrangement.Center\n            ) {\n\n                OutlinedButton(\n                    onClick = {\n                        angle = 0f\n                        scale = 1f\n                        offsetX = 0f\n                        offsetY = 0f\n                        matrix = Matrix()\n                    },\n                ) {\n                    Text(i18nState.getString(\"restore\"))\n                }\n            }\n\n            Column(\n                verticalArrangement = Arrangement.Center\n            ) {\n                Text(\n                    text = scale.to2fStr(),\n                    color = Color.Unspecified,\n                    modifier = Modifier.align(Alignment.CenterHorizontally)\n                )\n\n                verticalSlider(\n                    value = scale,\n                    onValueChange = {\n                        scale = it\n                    },\n                    modifier = Modifier\n                        .width(200.dp)\n                        .height(50.dp)\n                        .background(Color(0xffdedede)),\n                    valueRange = 0.1f..5f\n                )\n            }\n        }\n    }\n}\n\n@Composable\nfun verticalSlider(\n    value: Float,\n    onValueChange: (Float) -> Unit,\n    modifier: Modifier = Modifier,\n    enabled: Boolean = true,\n    valueRange: ClosedFloatingPointRange<Float> = 0f..1f,\n    /*@IntRange(from = 0)*/\n    steps: Int = 0,\n    onValueChangeFinished: (() -> Unit)? = null,\n    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },\n    colors: SliderColors = SliderDefaults.colors()\n){\n    val focusRequester = remember { FocusRequester() }\n    \n    Slider(\n        colors = colors,\n        interactionSource = interactionSource,\n        onValueChangeFinished = onValueChangeFinished,\n        steps = steps,\n        valueRange = valueRange,\n        enabled = enabled,\n        value = value,\n        onValueChange = onValueChange,\n        modifier = Modifier\n            .focusRequester(focusRequester)\n            .graphicsLayer {\n                rotationZ = 270f\n                transformOrigin = TransformOrigin(0f, 0f)\n            }\n            .layout { measurable, constraints ->\n                val placeable = measurable.measure(\n                    Constraints(\n                        minWidth = constraints.minHeight,\n                        maxWidth = constraints.maxHeight,\n                        minHeight = constraints.minWidth,\n                        maxHeight = constraints.maxHeight,\n                    )\n                )\n                layout(placeable.height, placeable.width) {\n                    placeable.place(-placeable.width, 0)\n                }\n            }\n            .then(modifier)\n    )\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/theme/ColorTheme.kt",
    "content": "package cn.netdiscovery.monica.ui.theme\n\nimport androidx.compose.ui.graphics.Color\nimport cn.netdiscovery.monica.i18n.LocalizationManager\nimport cn.netdiscovery.monica.i18n.Language\n\n/**\n * 颜色主题枚举\n * @author: Tony Shen\n * @date: 2025/9/8\n * @version: V1.0\n */\nenum class ColorTheme(\n    val displayName: String,\n    val primary: Color,\n    val primaryVariant: Color,\n    val secondary: Color,\n    val secondaryVariant: Color,\n    val background: Color,\n    val surface: Color,\n    val error: Color,\n    val onPrimary: Color,\n    val onSecondary: Color,\n    val onBackground: Color,\n    val onSurface: Color,\n    val onError: Color\n) {\n    LIGHT(\n        displayName = \"浅色主题\",\n        primary = Color(0xFF2196F3),\n        primaryVariant = Color(0xFF1976D2),\n        secondary = Color(0xFF03DAC6),\n        secondaryVariant = Color(0xFF018786),\n        background = Color(0xFFF5F5F5),\n        surface = Color(0xFFFFFFFF),\n        error = Color(0xFFB00020),\n        onPrimary = Color(0xFFFFFFFF),\n        onSecondary = Color(0xFF000000),\n        onBackground = Color(0xFF000000),\n        onSurface = Color(0xFF000000),\n        onError = Color(0xFFFFFFFF)\n    ),\n    \n    DARK(\n        displayName = \"深色主题\",\n        primary = Color(0xFF90CAF9),\n        primaryVariant = Color(0xFF42A5F5),\n        secondary = Color(0xFF03DAC6),\n        secondaryVariant = Color(0xFF018786),\n        background = Color(0xFF1A1A1A), // 稍微亮一点的深色\n        surface = Color(0xFF2D2D2D), // 更亮的表面色\n        error = Color(0xFFCF6679),\n        onPrimary = Color(0xFF000000),\n        onSecondary = Color(0xFF000000),\n        onBackground = Color(0xFFE0E0E0), // 更亮的文字色\n        onSurface = Color(0xFFE0E0E0), // 更亮的文字色\n        onError = Color(0xFF000000)\n    ),\n    \n    BLUE(\n        displayName = \"蓝色主题\",\n        primary = Color(0xFF1976D2),\n        primaryVariant = Color(0xFF0D47A1),\n        secondary = Color(0xFF03DAC6),\n        secondaryVariant = Color(0xFF018786),\n        background = Color(0xFFE3F2FD),\n        surface = Color(0xFFFFFFFF),\n        error = Color(0xFFB00020),\n        onPrimary = Color(0xFFFFFFFF),\n        onSecondary = Color(0xFF000000),\n        onBackground = Color(0xFF000000),\n        onSurface = Color(0xFF000000),\n        onError = Color(0xFFFFFFFF)\n    ),\n    \n    GREEN(\n        displayName = \"绿色主题\",\n        primary = Color(0xFF388E3C),\n        primaryVariant = Color(0xFF1B5E20),\n        secondary = Color(0xFF03DAC6),\n        secondaryVariant = Color(0xFF018786),\n        background = Color(0xFFE8F5E8),\n        surface = Color(0xFFFFFFFF),\n        error = Color(0xFFB00020),\n        onPrimary = Color(0xFFFFFFFF),\n        onSecondary = Color(0xFF000000),\n        onBackground = Color(0xFF000000),\n        onSurface = Color(0xFF000000),\n        onError = Color(0xFFFFFFFF)\n    ),\n    \n    PURPLE(\n        displayName = \"紫色主题\",\n        primary = Color(0xFF7B1FA2),\n        primaryVariant = Color(0xFF4A148C),\n        secondary = Color(0xFF03DAC6),\n        secondaryVariant = Color(0xFF018786),\n        background = Color(0xFFF3E5F5),\n        surface = Color(0xFFFFFFFF),\n        error = Color(0xFFB00020),\n        onPrimary = Color(0xFFFFFFFF),\n        onSecondary = Color(0xFF000000),\n        onBackground = Color(0xFF000000),\n        onSurface = Color(0xFF000000),\n        onError = Color(0xFFFFFFFF)\n    ),\n    \n    ORANGE(\n        displayName = \"橙色主题\",\n        primary = Color(0xFFF57C00),\n        primaryVariant = Color(0xFFE65100),\n        secondary = Color(0xFF03DAC6),\n        secondaryVariant = Color(0xFF018786),\n        background = Color(0xFFFFF3E0),\n        surface = Color(0xFFFFFFFF),\n        error = Color(0xFFB00020),\n        onPrimary = Color(0xFFFFFFFF),\n        onSecondary = Color(0xFF000000),\n        onBackground = Color(0xFF000000),\n        onSurface = Color(0xFF000000),\n        onError = Color(0xFFFFFFFF)\n    ),\n    \n    PINK(\n        displayName = \"粉色主题\",\n        primary = Color(0xFFE91E63),\n        primaryVariant = Color(0xFFC2185B),\n        secondary = Color(0xFFF06292),\n        secondaryVariant = Color(0xFFE91E63),\n        background = Color(0xFFFCE4EC),\n        surface = Color(0xFFF8BBD9),\n        error = Color(0xFFB00020),\n        onPrimary = Color(0xFFFFFFFF),\n        onSecondary = Color(0xFF000000),\n        onBackground = Color(0xFF000000),\n        onSurface = Color(0xFF000000),\n        onError = Color(0xFFFFFFFF)\n    );\n\n    /**\n     * 获取主题的显示名称\n     */\n    fun getThemeDisplayName(): String {\n        return when (this) {\n            LIGHT -> if (LocalizationManager.currentLanguage == Language.CHINESE) \"浅色主题\" else \"Light Theme\"\n            DARK -> if (LocalizationManager.currentLanguage == Language.CHINESE) \"深色主题\" else \"Dark Theme\"\n            BLUE -> if (LocalizationManager.currentLanguage == Language.CHINESE) \"蓝色主题\" else \"Blue Theme\"\n            GREEN -> if (LocalizationManager.currentLanguage == Language.CHINESE) \"绿色主题\" else \"Green Theme\"\n            PURPLE -> if (LocalizationManager.currentLanguage == Language.CHINESE) \"紫色主题\" else \"Purple Theme\"\n            ORANGE -> if (LocalizationManager.currentLanguage == Language.CHINESE) \"橙色主题\" else \"Orange Theme\"\n            PINK -> if (LocalizationManager.currentLanguage == Language.CHINESE) \"粉色主题\" else \"Pink Theme\"\n        }\n    }\n\n    /**\n     * 获取主题的唯一标识符\n     */\n    fun getThemeId(): String = name\n}\n"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/theme/ThemeManager.kt",
    "content": "package cn.netdiscovery.monica.ui.theme\n\nimport androidx.compose.material.Colors\nimport androidx.compose.material.MaterialTheme\nimport androidx.compose.material.darkColors\nimport androidx.compose.material.lightColors\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport androidx.compose.runtime.staticCompositionLocalOf\nimport androidx.compose.ui.graphics.Color\nimport org.slf4j.Logger\nimport org.slf4j.LoggerFactory\n\n/**\n * 主题管理器\n * @author: Tony Shen\n * @date: 2025/9/8\n * @version: V1.0\n */\nobject ThemeManager {\n    private val logger: Logger = LoggerFactory.getLogger(ThemeManager::class.java)\n    \n    // 移除独立的状态管理，改为从外部获取\n    private var _currentTheme: ColorTheme? = null\n    \n    /**\n     * 设置当前主题（从ApplicationState调用）\n     */\n    fun setCurrentTheme(theme: ColorTheme) {\n        logger.info(\"切换主题: ${theme.displayName}\")\n        _currentTheme = theme\n    }\n    \n    /**\n     * 获取当前主题\n     */\n    fun getCurrentTheme(): ColorTheme {\n        return _currentTheme ?: ColorTheme.LIGHT\n    }\n    \n    /**\n     * 根据主题获取 Material Colors\n     * 根据背景亮度自动选择使用 lightColors 或 darkColors\n     */\n    fun getMaterialColors(theme: ColorTheme = getCurrentTheme()): Colors {\n        // 计算背景色的亮度来判断是否为深色主题\n        val isDarkTheme = isDarkBackground(theme.background)\n        \n        return if (isDarkTheme) {\n            darkColors(\n                primary = theme.primary,\n                primaryVariant = theme.primaryVariant,\n                secondary = theme.secondary,\n                secondaryVariant = theme.secondaryVariant,\n                background = theme.background,\n                surface = theme.surface,\n                error = theme.error,\n                onPrimary = theme.onPrimary,\n                onSecondary = theme.onSecondary,\n                onBackground = theme.onBackground,\n                onSurface = theme.onSurface,\n                onError = theme.onError\n            )\n        } else {\n            lightColors(\n                primary = theme.primary,\n                primaryVariant = theme.primaryVariant,\n                secondary = theme.secondary,\n                secondaryVariant = theme.secondaryVariant,\n                background = theme.background,\n                surface = theme.surface,\n                error = theme.error,\n                onPrimary = theme.onPrimary,\n                onSecondary = theme.onSecondary,\n                onBackground = theme.onBackground,\n                onSurface = theme.onSurface,\n                onError = theme.onError\n            )\n        }\n    }\n    \n    /**\n     * 判断背景色是否为深色\n     * 使用相对亮度公式：0.299*R + 0.587*G + 0.114*B\n     */\n    private fun isDarkBackground(backgroundColor: Color): Boolean {\n        val luminance = 0.299f * backgroundColor.red + 0.587f * backgroundColor.green + 0.114f * backgroundColor.blue\n        return luminance < 0.5f // 亮度小于0.5认为是深色背景\n    }\n    \n    /**\n     * 获取所有可用主题\n     */\n    fun getAllThemes(): List<ColorTheme> {\n        return ColorTheme.values().toList()\n    }\n    \n    /**\n     * 根据 ID 获取主题\n     */\n    fun getThemeById(id: String): ColorTheme? {\n        return try {\n            ColorTheme.valueOf(id)\n        } catch (e: IllegalArgumentException) {\n            logger.warn(\"未找到主题: $id\")\n            null\n        }\n    }\n    \n    /**\n     * 重置为默认主题\n     */\n    fun resetToDefault() {\n        logger.info(\"重置为默认主题\")\n        setCurrentTheme(ColorTheme.LIGHT)\n    }\n}\n\n/**\n * 主题状态的可组合函数\n */\n@Composable\nfun rememberThemeState(): ColorTheme {\n    return ThemeManager.getCurrentTheme()\n}\n\n/**\n * 主题切换的可组合函数\n */\n@Composable\nfun setTheme(theme: ColorTheme) {\n    ThemeManager.setCurrentTheme(theme)\n}\n\n/**\n * 自定义 MaterialTheme 包装器\n */\n@Composable\nfun CustomMaterialTheme(\n    theme: ColorTheme = ThemeManager.getCurrentTheme(),\n    content: @Composable () -> Unit\n) {\n    val colors = ThemeManager.getMaterialColors(theme)\n    \n    MaterialTheme(\n        colors = colors,\n        content = content\n    )\n}\n\n/**\n * 主题状态提供者\n */\nval LocalThemeState = staticCompositionLocalOf { ColorTheme.LIGHT }"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/widget/Buttons.kt",
    "content": "package cn.netdiscovery.monica.ui.widget\n\nimport androidx.compose.foundation.ExperimentalFoundationApi\nimport androidx.compose.foundation.TooltipArea\nimport androidx.compose.foundation.TooltipPlacement\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.*\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.shadow\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.painter.Painter\nimport androidx.compose.ui.unit.DpOffset\nimport androidx.compose.ui.unit.dp\nimport cn.netdiscovery.monica.utils.Action\nimport org.slf4j.Logger\nimport org.slf4j.LoggerFactory\nimport cn.netdiscovery.monica.i18n.LocalizationManager\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.widget.Buttons\n * @author: Tony Shen\n * @date: 2024/5/11 10:46\n * @version: V1.0 <描述当前版本功能>\n */\nval logger: Logger = LoggerFactory.getLogger(object : Any() {}.javaClass.enclosingClass)\n\nconst val VIEW_CLICK_INTERVAL_TIME = 300 // View 的 click 方法的两次点击间隔时间，减少到300ms提高响应性\n\n/**\n * 可复用的点击节流函数，支持状态隔离、高精度时间、加载状态拦截与过滤函数。\n */\n@Composable\nfun rememberThrottledClick(\n    intervalMs: Int = VIEW_CLICK_INTERVAL_TIME,\n    isLoading: Boolean = false,\n    filter: () -> Boolean = { true },\n    onClick: Action\n): Action {\n    // 使用 nanoTime，避免 system time 被修改时导致节流失效\n    var lastClickNanoTime by remember { mutableStateOf(0L) }\n    val intervalNs = intervalMs * 1_000_000L\n\n    return {\n        val now = System.nanoTime()\n        val elapsed = now - lastClickNanoTime\n\n        if (elapsed >= intervalNs && !isLoading && filter()) {\n            lastClickNanoTime = now\n            onClick()\n        }\n    }\n}\n\n@OptIn(ExperimentalFoundationApi::class)\n@Composable\nfun toolTipButton(\n    text:String,\n    painter: Painter,\n    buttonModifier: Modifier = Modifier.padding(5.dp),\n    iconModifier: Modifier = Modifier.size(36.dp),\n    enable: ()-> Boolean = { true },\n    onClick: Action,\n) {\n    TooltipArea(\n        tooltip = {\n            // composable tooltip content\n            Surface(\n                modifier = Modifier.shadow(4.dp),\n                color = Color(255, 255, 210),\n                shape = RoundedCornerShape(4.dp)\n            ) {\n                Text(\n                    text = text,\n                    modifier = Modifier.padding(10.dp)\n                )\n            }\n        },\n        delayMillis = 600, // in milliseconds\n        tooltipPlacement = TooltipPlacement.CursorPoint(\n            alignment = Alignment.BottomEnd,\n            offset = DpOffset((-16).dp, 0.dp)\n        )\n    ) {\n        IconButton(\n            modifier = buttonModifier.padding(4.dp), // 增加额外的padding扩大点击区域\n            onClick =  rememberThrottledClick { // 防止重复点击，300ms内只有1次点击是有效的\n\n                logger.info(\"点击了 $text 按钮\")\n                onClick()\n            },\n            enabled = enable()\n        ) {\n            Icon(\n                painter = painter,\n                contentDescription = text,\n                modifier = iconModifier\n            )\n        }\n    }\n}\n\n@Composable\nfun confirmButton(enabled:Boolean,\n                  text:String = LocalizationManager.getString(\"confirm\"),\n                  modifier:Modifier = Modifier,\n                  onClick: () -> Unit) {\n    Button(\n        modifier = modifier,\n        onClick = rememberThrottledClick {\n            onClick.invoke()\n        },\n        enabled = enabled\n    ) {\n        Text(text = text,\n            color = if (enabled) Color.Unspecified else Color.LightGray)\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/widget/Checkboxs.kt",
    "content": "package cn.netdiscovery.monica.ui.widget\n\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.material.Checkbox\nimport androidx.compose.material.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.TextUnit\nimport androidx.compose.material.MaterialTheme\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.widget.Checkboxs\n * @author: Tony Shen\n * @date: 2024/10/28 13:50\n * @version: V1.0 <描述当前版本功能>\n */\n\n@Composable\nfun checkBoxWithTitle(\n    text: String,\n    modifier: Modifier = Modifier,\n    textModifier: Modifier = Modifier,\n    color: Color = Color.Unspecified,\n    checked: Boolean,\n    onCheckedChange: ((Boolean) -> Unit)?,\n    fontSize: TextUnit = MaterialTheme.typography.body1.fontSize,\n    fontWeight: FontWeight? = null\n) {\n    Row(\n        modifier = modifier,\n        verticalAlignment = Alignment.CenterVertically\n    ) {\n        Checkbox(\n            checked = checked,\n            onCheckedChange = { onCheckedChange?.invoke(it) }\n        )\n        Text(\n            text = text,\n            modifier = textModifier,\n            color = color,\n            fontSize = fontSize,\n            fontWeight = fontWeight\n        )\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/widget/Divider.kt",
    "content": "package cn.netdiscovery.monica.ui.widget\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.unit.dp\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.widget.Divider\n * @author: Tony Shen\n * @date: 2024/10/2 22:13\n * @version: V1.0 <描述当前版本功能>\n */\n\n@Composable\nfun divider() {\n    Row {\n        Spacer(modifier = Modifier.padding(top = 10.dp, bottom = 10.dp).height(1.dp).weight(1.0f).background(color = Color.LightGray))\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/widget/LazyRow.kt",
    "content": "package cn.netdiscovery.monica.ui.widget\n\nimport androidx.compose.foundation.gestures.Orientation\nimport androidx.compose.foundation.gestures.draggable\nimport androidx.compose.foundation.gestures.rememberDraggableState\nimport androidx.compose.foundation.gestures.scrollBy\nimport androidx.compose.foundation.lazy.LazyRow\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.ui.Modifier\nimport kotlinx.coroutines.launch\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.widget.LazyRow\n * @author: Tony Shen\n * @date: 2024/7/13 22:27\n * @version: V1.0 <描述当前版本功能>\n */\n@Composable\nfun desktopLazyRow(modifier:Modifier = Modifier, content: @Composable () -> Unit) {\n    val scrollState = rememberLazyListState()\n    val coroutineScope = rememberCoroutineScope()\n\n    LazyRow(\n        state = scrollState,\n        modifier = modifier\n            .draggable(\n                orientation = Orientation.Horizontal,\n                state = rememberDraggableState { delta ->\n                    coroutineScope.launch {\n                        scrollState.scrollBy(-delta)\n                    }\n                },\n            )\n    ) {\n        item {\n            content.invoke()\n        }\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/widget/PageLifecycle.kt",
    "content": "package cn.netdiscovery.monica.ui.widget\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.DisposableEffect\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.widget.PageLifecycle\n * @author: Tony Shen\n * @date: 2025/6/17 15:45\n * @version: V1.0 <描述当前版本功能>\n */\n\n/**\n * 页面生命周期钩子，用于页面级别的生命周期管理。在页面进入时执行初始化逻辑，在页面移除时释放资源。\n *\n *  @param onInit 页面进入时的初始化逻辑（支持 suspend 函数）\n *  @param onDisposeEffect 页面被移除时的清理逻辑（同步函数）\n */\n@Composable\nfun PageLifecycle(\n    onInit: suspend () -> Unit,\n    onDisposeEffect: () -> Unit\n) {\n    // 页面进入时执行（支持挂起函数）\n    LaunchedEffect(Unit) {\n        onInit()\n    }\n\n    // 页面移除时执行（释放资源、取消监听等）\n    DisposableEffect(Unit) {\n        onDispose {\n            onDisposeEffect()\n        }\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/widget/RightSideMenuBar.kt",
    "content": "package cn.netdiscovery.monica.ui.widget\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.unit.dp\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.widget.RightSideMenuBar\n * @author: Tony Shen\n * @date: 2024/10/5 14:22\n * @version: V1.0 <描述当前版本功能>\n */\n@Composable\nfun rightSideMenuBar(modifier: Modifier,\n                     backgroundColor:Color = Color.LightGray,\n                     percent:Int = 15,\n                     content: @Composable ColumnScope.() -> Unit) {\n\n    Row(modifier = modifier\n        .padding(start = 10.dp, end = 10.dp)\n        .background(color = backgroundColor, shape = RoundedCornerShape(percent))) {\n        Column(\n            Modifier.padding(start = 10.dp, end = 10.dp, top = 20.dp, bottom = 20.dp),\n            verticalArrangement = Arrangement.Center\n        ) {\n            content.invoke(this)\n        }\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/widget/TextFields.kt",
    "content": "package cn.netdiscovery.monica.ui.widget\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.foundation.text.BasicTextField\nimport androidx.compose.foundation.text.KeyboardActions\nimport androidx.compose.foundation.text.KeyboardOptions\nimport androidx.compose.material.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.text.TextStyle\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.widget.TextFields\n * @author: Tony Shen\n * @date: 2024/10/17 11:17\n * @version: V1.0 <描述当前版本功能>\n */\n@Composable\nfun basicTextFieldWithTitle(\n    titleText:String,\n    value: String,\n    modifier:Modifier = Modifier,\n    textModifier:Modifier = Modifier,\n    width: Dp = 120.dp,\n    onValueChange: (String) -> Unit) {\n    Row {\n        Text(text = titleText, modifier = textModifier)\n\n        BasicTextField(\n            value = value,\n            onValueChange = onValueChange,\n            keyboardOptions = KeyboardOptions.Default,\n            keyboardActions = KeyboardActions.Default,\n            cursorBrush = SolidColor(Color.Gray),\n            singleLine = true,\n            modifier = modifier.padding(start = 10.dp, end = 10.dp).width(width).background(Color.LightGray.copy(alpha = 0.5f), shape = RoundedCornerShape(3.dp)).height(20.dp),\n            textStyle = TextStyle(Color.Black, fontSize = 12.sp)\n        )\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/widget/ThreeBallLoading.kt",
    "content": "package cn.netdiscovery.monica.ui.widget\n\nimport androidx.compose.animation.core.*\nimport androidx.compose.foundation.Canvas\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.MaterialTheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.unit.dp\nimport cn.netdiscovery.monica.config.height\nimport cn.netdiscovery.monica.config.loadingWidth\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.flow.flow\nimport kotlin.math.cos\nimport kotlin.math.sin\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.ThreeBallLoading\n * @author: Tony Shen\n * @date: 2024/4/28 17:45\n * @version: V1.0 <描述当前版本功能>\n */\n@Composable\nfun showLoading() {\n    Box(\n        modifier = Modifier.fillMaxSize(),\n        contentAlignment = Alignment.Center\n    ) {\n        ThreeBallLoading(\n            modifier = Modifier\n                .width(loadingWidth)\n                .height(height)\n                .background(\n                    color = Color.Transparent,\n                    shape = RoundedCornerShape(16.dp)\n                )\n                .padding(20.dp)\n        )\n    }\n}\n\n@Composable\nfun ThreeBallLoading(modifier: Modifier) {\n    val width = remember { mutableStateOf(800f) }\n    val height = remember { mutableStateOf(800f) }\n    val centerX = width.value / 2\n    val centerY = height.value / 2\n    val anglist = Array(3) {\n        120f * it\n    }\n    val ballradius = 20f\n    val colorList = listOf(\n        Color(0xffFF1D1D),\n        Color(0xff0055FF),\n        Color(0xff43B988),\n    )\n    val transition = rememberInfiniteTransition()\n    val radiusDiff = transition.animateFloat(\n        ballradius / 2, ballradius * 4, animationSpec = InfiniteRepeatableSpec(\n            tween(durationMillis = 500, easing = LinearEasing), repeatMode = RepeatMode.Reverse\n        )\n    )\n    val diff = remember { mutableStateOf(0f) }\n    LaunchedEffect(true) {\n        flow {\n            while (true) {\n                emit(1)\n                delay(1000)\n            }\n        }.collect {\n            diff.value += 90f\n        }\n    }\n    val angleDiff = animateFloatAsState(\n        diff.value, TweenSpec(\n            durationMillis = 500, easing = LinearEasing\n        )\n    )\n    Canvas(\n        modifier = modifier.padding(10.dp)\n    ) {\n        width.value = size.width\n        height.value = size.height\n\n        for (index in anglist.indices) {\n            drawCircle(\n                colorList[index], radius = 20f, center = Offset(\n                    pointX(radiusDiff.value, centerX, anglist[index] + angleDiff.value),\n                    pointY(radiusDiff.value, centerY, anglist[index] + angleDiff.value)\n                )\n            )\n        }\n\n    }\n}\n\nprivate fun pointX(radius: Float, centerX: Float, angle: Float): Float {\n    val res = Math.toRadians(angle.toDouble())\n    return centerX - cos(res).toFloat() * (radius)\n}\n\nprivate fun pointY(radius: Float, centerY: Float, angle: Float): Float {\n    val res = Math.toRadians(angle.toDouble())\n    return centerY - sin(res).toFloat() * (radius)\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/widget/Title.kt",
    "content": "package cn.netdiscovery.monica.ui.widget\n\nimport androidx.compose.material.MaterialTheme\nimport androidx.compose.material.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.TextUnit\nimport cn.netdiscovery.monica.config.subTitleTextSize\nimport cn.netdiscovery.monica.config.titleTextSize\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.widget.Title\n * @author: Tony Shen\n * @date: 2024/10/2 22:22\n * @version: V1.0 <描述当前版本功能>\n */\n\n@Composable\nfun title(\n    modifier: Modifier = Modifier,\n    text: String,\n    color: Color = MaterialTheme.colors.primary,\n    fontSize: TextUnit = titleTextSize,\n    fontWeight: FontWeight? = null\n) {\n    Text(\n        modifier = modifier,\n        text = text,\n        color = color,\n        fontSize = fontSize,\n        fontWeight = fontWeight\n    )\n}\n\n@Composable\nfun subTitle(\n    modifier: Modifier = Modifier,\n    text: String,\n    color: Color = MaterialTheme.colors.primary,\n    fontSize: TextUnit = subTitleTextSize,\n    fontWeight: FontWeight? = null\n) {\n    Text(\n        modifier = modifier,\n        text = text,\n        color = color,\n        fontSize = fontSize,\n        fontWeight = fontWeight\n    )\n}\n\n@Composable\nfun subTitleWithDivider(\n    modifier: Modifier = Modifier,\n    text: String,\n    color: Color = MaterialTheme.colors.primary,\n    fontSize: TextUnit = subTitleTextSize,\n    fontWeight: FontWeight? = null\n) {\n    subTitle(modifier = modifier, text = text, color = color, fontSize = fontSize, fontWeight = fontWeight)\n    divider()\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/widget/Toasts.kt",
    "content": "package cn.netdiscovery.monica.ui.widget\n\nimport androidx.compose.animation.core.FastOutSlowInEasing\nimport androidx.compose.animation.core.animateDpAsState\nimport androidx.compose.animation.core.tween\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.MaterialTheme\nimport androidx.compose.material.Text\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.TextUnit\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport cn.netdiscovery.monica.utils.Action\nimport kotlinx.coroutines.delay\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.widget.Toasts\n * @author: Tony Shen\n * @date: 2024/5/28 15:13\n * @version: V1.0 <描述当前版本功能>\n */\n@Composable\nfun topToast(\n    modifier: Modifier = Modifier,\n    message: String = \"\",\n    textColor: Color = Color.Gray,\n    fontSize: TextUnit = 16.sp,\n    height: Dp = 100.dp,\n    width: Dp = 400.dp,\n    onDismissCallback: Action = {},\n) {\n    toast(modifier = modifier,\n        message = message,\n        textColor = textColor,\n        fontSize = fontSize,\n        height = height,\n        width = width,\n        alignment = Alignment.TopCenter,\n        onDismissCallback = onDismissCallback)\n}\n\n@Composable\nfun centerToast(\n    modifier: Modifier = Modifier,\n    message: String = \"\",\n    textColor: Color = Color.Gray,\n    fontSize: TextUnit = 16.sp,\n    height: Dp = 100.dp,\n    width: Dp = 400.dp,\n    onDismissCallback: Action = {},\n) {\n    toast(modifier = modifier,\n        message = message,\n        textColor = textColor,\n        fontSize = fontSize,\n        height = height,\n        width = width,\n        alignment = Alignment.Center,\n        onDismissCallback = onDismissCallback)\n}\n\n@Composable\nfun bottomToast(\n    modifier: Modifier = Modifier,\n    message: String = \"\",\n    textColor: Color = Color.Gray,\n    fontSize: TextUnit = 16.sp,\n    height: Dp = 100.dp,\n    width: Dp = 400.dp,\n    onDismissCallback: Action = {},\n) {\n    toast(modifier = modifier,\n        message = message,\n        textColor = textColor,\n        fontSize = fontSize,\n        height = height,\n        width = width,\n        alignment = Alignment.BottomCenter,\n        onDismissCallback = onDismissCallback)\n}\n\n@Composable\nprivate fun toast(\n    modifier: Modifier = Modifier,\n    message: String = \"An unexpected error occurred. Please try again later\",\n    textColor: Color = Color.Black,\n    fontSize: TextUnit = 16.sp,\n    height: Dp = 100.dp,\n    width: Dp = 400.dp,\n    alignment: Alignment,\n    onDismissCallback: Action = {}\n) {\n    var hasTransitionStarted by remember { mutableStateOf(false) }\n    var clipShape by remember { mutableStateOf(CircleShape) }\n    var slideDownAnimation by remember { mutableStateOf(true) }\n    var animationStarted by remember { mutableStateOf(false) }\n    var showMessage by remember { mutableStateOf(false) }\n    var dismissCallback by remember { mutableStateOf(false) }\n\n    val boxWidth by animateDpAsState(\n        targetValue = if (hasTransitionStarted) width else 30.dp,\n        animationSpec = tween(durationMillis = 100, easing = FastOutSlowInEasing),\n        label = \"Box width\",\n    )\n\n    val boxHeight by animateDpAsState(\n        targetValue = if (hasTransitionStarted) height else 30.dp,\n        animationSpec = tween(durationMillis = 100, easing = FastOutSlowInEasing),\n        label = \"Box height\",\n    )\n\n    val slideY by animateDpAsState(\n        targetValue = if (slideDownAnimation) (-100).dp else 0.dp,\n        animationSpec = tween(durationMillis = 100),\n        label = \"Slide parameter in DP\",\n    )\n\n    LaunchedEffect(message) {\n        // 重置状态\n        hasTransitionStarted = false\n        clipShape = CircleShape\n        slideDownAnimation = true\n        animationStarted = false\n        showMessage = false\n        dismissCallback = false\n        \n        slideDownAnimation = false\n\n        // Delay for 0.2 seconds before transitioning to rectangle\n        delay(200)\n        hasTransitionStarted = true\n        clipShape = RoundedCornerShape(12.dp, 12.dp, 12.dp, 12.dp)\n        showMessage = true\n\n        // Delay for 2.5 seconds before reverting to circle\n        delay(2500)\n        hasTransitionStarted = false\n        showMessage = false\n\n        // Delay for 0.2 seconds before sliding up\n        delay(200)\n        clipShape = CircleShape\n        slideDownAnimation = true\n        animationStarted = true\n        dismissCallback = true\n    }\n\n    Box(\n        modifier = Modifier\n            .fillMaxSize()\n            .background(Color.Transparent)\n            .padding(16.dp),\n    ) {\n        Box(\n            modifier = modifier\n                .size(boxWidth, boxHeight)\n                .offset(y = slideY)\n                .clip(clipShape)\n                .background(MaterialTheme.colors.primary.copy(0.7f))\n                .align(alignment = alignment),\n            contentAlignment = Alignment.Center,\n        ) {\n            if (showMessage) {\n                Text(\n                    text = message,\n                    color = textColor,\n                    fontWeight = FontWeight.Bold,\n                    fontSize = fontSize,\n                    textAlign = TextAlign.Center,\n                    modifier = Modifier\n                        .padding(16.dp),\n                )\n            }\n\n            if (dismissCallback) onDismissCallback()\n        }\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/widget/color/ColorSelection.kt",
    "content": "package cn.netdiscovery.monica.ui.widget.color\n\nimport androidx.compose.foundation.Canvas\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.material.Slider\nimport androidx.compose.material.SliderDefaults\nimport androidx.compose.material.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.drawscope.Stroke\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.showimage.ColorSelection\n * @author: Tony Shen\n * @date: 2024/5/19 10:43\n * @version: V1.0 <描述当前版本功能>\n */\nval gradientColors = listOf(\n    Color.Red,\n    Color.Magenta,\n    Color.Blue,\n    Color.Cyan,\n    Color.Green,\n    Color.Yellow,\n    Color.Red\n)\n\n@Composable\nfun ColorWheel(modifier: Modifier = Modifier) {\n\n    Canvas(modifier = modifier) {\n        val canvasWidth = size.width\n        val canvasHeight = size.height\n\n        require(canvasWidth == canvasHeight,\n            lazyMessage = {\n                print(\"Canvas dimensions should be equal to each other\")\n            }\n        )\n        val cX = canvasWidth / 2\n        val cY = canvasHeight / 2\n        val canvasRadius = canvasWidth.coerceAtMost(canvasHeight) / 2f\n        val center = Offset(cX, cY)\n        val strokeWidth = canvasRadius * .3f\n        // Stroke is drawn out of the radius, so it's required to subtract stroke width from radius\n        val radius = canvasRadius - strokeWidth\n\n        drawCircle(\n            brush = Brush.sweepGradient(colors = gradientColors, center = center),\n            radius = radius,\n            center = center,\n            style = Stroke(\n                width = strokeWidth\n            )\n        )\n    }\n}\n\n/**\n * Composable that shows a title as initial letter, title color and a Slider to pick color\n */\n@Composable\nfun ColorSlider(\n    modifier: Modifier,\n    title: String,\n    titleColor: Color,\n    valueRange: ClosedFloatingPointRange<Float> = 0f..255f,\n    rgb: Float,\n    onColorChanged: (Float) -> Unit\n) {\n    val focusRequester = remember { FocusRequester() }\n    \n    Row(modifier, verticalAlignment = Alignment.CenterVertically) {\n\n        Text(text = title.take(1), color = titleColor, fontWeight = FontWeight.Bold)\n        Spacer(modifier = Modifier.width(8.dp))\n        Slider(\n            modifier = Modifier.weight(1f).focusRequester(focusRequester),\n            value = rgb,\n            onValueChange = { onColorChanged(it) },\n            valueRange = valueRange,\n            onValueChangeFinished = {},\n            colors = SliderDefaults.colors(\n                thumbColor = titleColor,\n                activeTrackColor = titleColor\n            )\n        )\n\n        Spacer(modifier = Modifier.width(8.dp))\n        Text(\n            text = rgb.toInt().toString(),\n            color = Color.LightGray,\n            fontSize = 12.sp,\n            modifier = Modifier.width(30.dp)\n        )\n\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/widget/color/ColorSelectionDialog.kt",
    "content": "package cn.netdiscovery.monica.ui.widget.color\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.*\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.shadow\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport androidx.compose.ui.window.Dialog\nimport kotlin.math.roundToInt\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.showimage.ColorSelectionDialog\n * @author: Tony Shen\n * @date: 2024/5/19 10:41\n * @version: V1.0 <描述当前版本功能>\n */\nval Blue400 = Color(0xff42A5F5)\n\n@Composable\nfun ColorSelectionDialog(\n    initialColor: Color,\n    onDismiss: () -> Unit,\n    onNegativeClick: () -> Unit,\n    onPositiveClick: (Color) -> Unit\n) {\n    var red by remember { mutableStateOf(initialColor.red * 255) }\n    var green by remember { mutableStateOf(initialColor.green * 255) }\n    var blue by remember { mutableStateOf(initialColor.blue * 255) }\n    var alpha by remember { mutableStateOf(initialColor.alpha * 255) }\n\n    val color = Color(\n        red = red.roundToInt(),\n        green = green.roundToInt(),\n        blue = blue.roundToInt(),\n        alpha = alpha.roundToInt()\n    )\n\n    Dialog(onDismissRequest = onDismiss) {\n\n        BoxWithConstraints(\n            Modifier\n                .shadow(1.dp, RoundedCornerShape(8.dp))\n                .background(Color.White)\n        ) {\n\n            val widthInDp = LocalDensity.current.run { maxWidth }\n\n\n            Column(horizontalAlignment = Alignment.CenterHorizontally) {\n\n                Text(\n                    text = \"Color\",\n                    color = Blue400,\n                    fontSize = 18.sp,\n                    fontWeight = FontWeight.Bold,\n                    modifier = Modifier.padding(top = 12.dp)\n                )\n\n                // Initial and Current Colors\n                Row(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .padding(horizontal = 50.dp, vertical = 20.dp)\n                ) {\n\n                    Box(\n                        modifier = Modifier\n                            .weight(1f)\n                            .height(40.dp)\n                            .background(\n                                initialColor,\n                                shape = RoundedCornerShape(topStart = 8.dp, bottomStart = 8.dp)\n                            )\n                    )\n                    Box(\n                        modifier = Modifier\n                            .weight(1f)\n                            .height(40.dp)\n                            .background(\n                                color,\n                                shape = RoundedCornerShape(topEnd = 8.dp, bottomEnd = 8.dp)\n                            )\n                    )\n                }\n\n                ColorWheel(\n                    modifier = Modifier\n                        .width(widthInDp * .8f)\n                        .aspectRatio(1f)\n                )\n\n                Spacer(modifier = Modifier.height(16.dp))\n\n                // Sliders\n                ColorSlider(\n                    modifier = Modifier\n                        .padding(start = 12.dp, end = 12.dp)\n                        .fillMaxWidth(),\n                    title = \"Red\",\n                    titleColor = Color.Red,\n                    rgb = red,\n                    onColorChanged = {\n                        red = it\n                    }\n                )\n                Spacer(modifier = Modifier.height(4.dp))\n\n                ColorSlider(\n                    modifier = Modifier\n                        .padding(start = 12.dp, end = 12.dp)\n                        .fillMaxWidth(),\n                    title = \"Green\",\n                    titleColor = Color.Green,\n                    rgb = green,\n                    onColorChanged = {\n                        green = it\n                    }\n                )\n                Spacer(modifier = Modifier.height(4.dp))\n\n                ColorSlider(\n                    modifier = Modifier\n                        .padding(start = 12.dp, end = 12.dp)\n                        .fillMaxWidth(),\n                    title = \"Blue\",\n                    titleColor = Color.Blue,\n                    rgb = blue,\n                    onColorChanged = {\n                        blue = it\n                    }\n                )\n                Spacer(modifier = Modifier.height(4.dp))\n\n                ColorSlider(\n                    modifier = Modifier\n                        .padding(start = 12.dp, end = 12.dp)\n                        .fillMaxWidth(),\n                    title = \"Alpha\",\n                    titleColor = Color.Black,\n                    rgb = alpha,\n                    onColorChanged = {\n                        alpha = it\n                    }\n                )\n                Spacer(modifier = Modifier.height(24.dp))\n\n                // Buttons\n                Row(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .height(60.dp)\n                        .background(Color(0xffF3E5F5)),\n                    verticalAlignment = Alignment.CenterVertically\n\n                ) {\n                    TextButton(\n                        onClick = onNegativeClick,\n                        modifier = Modifier\n                            .weight(1f)\n                            .fillMaxHeight()\n                    ) {\n                        Text(text = \"CANCEL\")\n                    }\n\n                    TextButton(\n                        modifier = Modifier\n                            .weight(1f)\n                            .fillMaxHeight(),\n                        onClick = {\n                            onPositiveClick(color)\n                        },\n                    ) {\n                        Text(text = \"OK\")\n                    }\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/widget/image/ImageContentScaleUtil.kt",
    "content": "package cn.netdiscovery.monica.ui.widget.image\n\nimport androidx.compose.foundation.Canvas\nimport androidx.compose.foundation.layout.BoxWithConstraints\nimport androidx.compose.foundation.layout.BoxWithConstraintsScope\nimport androidx.compose.ui.graphics.Canvas\nimport androidx.compose.ui.graphics.ImageBitmap\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.unit.IntOffset\nimport androidx.compose.ui.unit.IntRect\nimport androidx.compose.ui.unit.IntSize\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.image.ImageContentScaleUtil\n * @author: Tony Shen\n * @date: 2024/5/14 15:47\n * @version: V1.0 <描述当前版本功能>\n */\n\n/**\n * Get Rectangle of [ImageBitmap] with [bitmapWidth] and [bitmapHeight] that is drawn inside\n * Canvas with [imageWidth] and [imageHeight]. [boxWidth] and [boxHeight] belong\n * to [BoxWithConstraints] that contains Canvas.\n *  @param boxWidth width of the parent container\n *  @param boxHeight height of the parent container\n *  @param imageWidth width of the [Canvas] that draws [ImageBitmap]\n *  @param imageHeight height of the [Canvas] that draws [ImageBitmap]\n *  @param bitmapWidth intrinsic width of the [ImageBitmap]\n *  @param bitmapHeight intrinsic height of the [ImageBitmap]\n *  @return [IntRect] that covers [ImageBitmap] bounds. When image [ContentScale] is crop\n *  this rectangle might return smaller rectangle than actual [ImageBitmap] and left or top\n *  of the rectangle might be bigger than zero.\n */\ninternal fun getScaledBitmapRect(\n    boxWidth: Int,\n    boxHeight: Int,\n    imageWidth: Float,\n    imageHeight: Float,\n    bitmapWidth: Int,\n    bitmapHeight: Int\n): IntRect {\n    // Get scale of box to width of the image\n    // We need a rect that contains Bitmap bounds to pass if any child requires it\n    // For a image with 100x100 px with 300x400 px container and image with crop 400x400px\n    // So we need to pass top left as 0,50 and size\n    val scaledBitmapX = boxWidth / imageWidth\n    val scaledBitmapY = boxHeight / imageHeight\n\n    val topLeft = IntOffset(\n        x = (bitmapWidth * (imageWidth - boxWidth) / imageWidth / 2)\n            .coerceAtLeast(0f).toInt(),\n        y = (bitmapHeight * (imageHeight - boxHeight) / imageHeight / 2)\n            .coerceAtLeast(0f).toInt()\n    )\n\n    val size = IntSize(\n        width = (bitmapWidth * scaledBitmapX).toInt().coerceAtMost(bitmapWidth),\n        height = (bitmapHeight * scaledBitmapY).toInt().coerceAtMost(bitmapHeight)\n    )\n\n    return IntRect(offset = topLeft, size = size)\n}\n\n/**\n * Get [IntSize] of the parent or container that contains [Canvas] that draws [ImageBitmap]\n *  @param bitmapWidth intrinsic width of the [ImageBitmap]\n *  @param bitmapHeight intrinsic height of the [ImageBitmap]\n *  @return size of parent Composable. When Modifier is assigned with fixed or finite size\n *  they are used, but when any dimension is set to infinity intrinsic dimensions of\n *  [ImageBitmap] are returned\n */\ninternal fun BoxWithConstraintsScope.getParentSize(\n    bitmapWidth: Int,\n    bitmapHeight: Int\n): IntSize {\n    // Check if Composable has fixed size dimensions\n    val hasBoundedDimens = constraints.hasBoundedWidth && constraints.hasBoundedHeight\n    // Check if Composable has infinite dimensions\n    val hasFixedDimens = constraints.hasFixedWidth && constraints.hasFixedHeight\n\n    // Box is the parent(BoxWithConstraints) that contains Canvas under the hood\n    // Canvas aspect ratio or size might not match parent but it's upper bounds are\n    // what are passed from parent. Canvas cannot be bigger or taller than BoxWithConstraints\n    val boxWidth: Int = if (hasBoundedDimens || hasFixedDimens) {\n        constraints.maxWidth\n    } else {\n        constraints.minWidth.coerceAtLeast(bitmapWidth)\n    }\n    val boxHeight: Int = if (hasBoundedDimens || hasFixedDimens) {\n        constraints.maxHeight\n    } else {\n        constraints.minHeight.coerceAtLeast(bitmapHeight)\n    }\n    return IntSize(boxWidth, boxHeight)\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/widget/image/ImageScope.kt",
    "content": "package cn.netdiscovery.monica.ui.widget.image\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.Stable\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.graphics.ImageBitmap\nimport androidx.compose.ui.graphics.toAwtImage\nimport androidx.compose.ui.graphics.toComposeImageBitmap\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.unit.Constraints\nimport androidx.compose.ui.unit.Density\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.IntRect\nimport cn.netdiscovery.monica.imageprocess.utils.extension.subImage\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.image.ImageScope\n * @author: Tony Shen\n * @date: 2024/5/14 15:25\n * @version: V1.0 <描述当前版本功能>\n */\n@Stable\ninterface ImageScope {\n    /**\n     * The constraints given by the parent layout in pixels.\n     *\n     * Use [minWidth], [maxWidth], [minHeight] or [maxHeight] if you need value in [Dp].\n     */\n    val constraints: Constraints\n\n    /**\n     * The minimum width in [Dp].\n     *\n     * @see constraints for the values in pixels.\n     */\n    val minWidth: Dp\n\n    /**\n     * The maximum width in [Dp].\n     *\n     * @see constraints for the values in pixels.\n     */\n    val maxWidth: Dp\n\n    /**\n     * The minimum height in [Dp].\n     *\n     * @see constraints for the values in pixels.\n     */\n    val minHeight: Dp\n\n    /**\n     * The maximum height in [Dp].\n     *\n     * @see constraints for the values in pixels.\n     */\n    val maxHeight: Dp\n\n    /**\n     * Width of area inside BoxWithConstraints that is scaled based on [ContentScale]\n     * This is width of the [Canvas] draw [ImageBitmap]\n     */\n    val imageWidth: Dp\n\n    /**\n     * Height of area inside BoxWithConstraints that is scaled based on [ContentScale]\n     * This is height of the [Canvas] draw [ImageBitmap]\n     */\n    val imageHeight: Dp\n\n    /**\n     * [IntRect] that covers boundaries of [ImageBitmap]\n     */\n    val rect: IntRect\n}\n\ninternal data class ImageScopeImpl(\n    private val density: Density,\n    override val constraints: Constraints,\n    override val imageWidth: Dp,\n    override val imageHeight: Dp,\n    override val rect: IntRect,\n) : ImageScope {\n\n    override val minWidth: Dp get() = with(density) { constraints.minWidth.toDp() }\n\n    override val maxWidth: Dp\n        get() = with(density) {\n            if (constraints.hasBoundedWidth) constraints.maxWidth.toDp() else Dp.Infinity\n        }\n\n    override val minHeight: Dp get() = with(density) { constraints.minHeight.toDp() }\n\n    override val maxHeight: Dp\n        get() = with(density) {\n            if (constraints.hasBoundedHeight) constraints.maxHeight.toDp() else Dp.Infinity\n        }\n}\n\n@Composable\ninternal fun getScaledImageBitmap(\n    imageWidth: Dp,\n    imageHeight: Dp,\n    rect: IntRect,\n    bitmap: ImageBitmap,\n    contentScale: ContentScale\n): ImageBitmap {\n\n    val scaledBitmap =\n        remember(bitmap, rect, imageWidth, imageHeight, contentScale) {\n            bitmap.toAwtImage().subImage(rect.left,rect.top,rect.width,rect.height).toComposeImageBitmap()\n        }\n    return scaledBitmap\n}\n\n@Composable\ninternal fun ImageScope.getScaledImageBitmap(\n    bitmap: ImageBitmap,\n    contentScale: ContentScale\n): ImageBitmap {\n\n    val scaledBitmap =\n        remember(bitmap, rect, imageWidth, imageHeight, contentScale) {\n            bitmap.toAwtImage().subImage(rect.left,rect.top,rect.width,rect.height).toComposeImageBitmap()\n        }\n    return scaledBitmap\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/widget/image/ImageSizeCalculator.kt",
    "content": "package cn.netdiscovery.monica.ui.widget.image\n\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.unit.dp\nimport cn.netdiscovery.monica.state.ApplicationState\nimport androidx.compose.ui.graphics.toComposeImageBitmap\nimport cn.netdiscovery.monica.ui.controlpanel.filter.viewmodel.FilterViewModel\nimport cn.netdiscovery.monica.utils.logger\nimport org.slf4j.Logger\nimport org.slf4j.LoggerFactory\n\n/**\n * 统一的图片尺寸计算工具\n * 确保在不同页面中图片显示大小一致\n * \n * @author Tony Shen\n * @date 2025/9/4\n * @version V1.0\n */\nobject ImageSizeCalculator {\n\n    private val logger: Logger = logger<ImageSizeCalculator>()\n    \n    // 默认最大尺寸配置 - 增加尺寸以支持更大的图片\n    private const val DEFAULT_MAX_WIDTH_DP = 1600f  // 从1200增加到1600\n    private const val DEFAULT_MAX_HEIGHT_DP = 1200f // 从800增加到1200\n    private const val MIN_DENSITY = 0.1f // 防止除零错误\n    \n    /**\n     * 计算统一的图片显示尺寸\n     * @param state 应用状态\n     * @param maxWidthDp 最大宽度（dp）\n     * @param maxHeightDp 最大高度（dp）\n     * @return 显示尺寸对\n     */\n    @androidx.compose.runtime.Composable\n    fun calculateImageSize(\n        state: ApplicationState,\n        maxWidthDp: Float = DEFAULT_MAX_WIDTH_DP,\n        maxHeightDp: Float = DEFAULT_MAX_HEIGHT_DP\n    ): Pair<androidx.compose.ui.unit.Dp, androidx.compose.ui.unit.Dp> {\n        val density = LocalDensity.current\n        \n        // 安全检查密度值\n        val safeDensity = if (density.density < MIN_DENSITY) {\n            logger.warn(\"检测到异常密度值: ${density.density}, 使用默认值 1.0\")\n            1.0f\n        } else {\n            density.density\n        }\n        \n        val image = state.currentImage?.toComposeImageBitmap()\n        \n        return if (image != null && image.width > 0 && image.height > 0) {\n            val bitmapWidth = image.width\n            val bitmapHeight = image.height\n            \n            // 原始图片尺寸（dp）\n            val originalWidthDp = bitmapWidth / safeDensity\n            val originalHeightDp = bitmapHeight / safeDensity\n            \n            // 计算缩放比例，保持宽高比\n            val scale = minOf(\n                maxWidthDp / originalWidthDp,\n                maxHeightDp / originalHeightDp\n            ).coerceAtMost(1f) // 不放大图片，只缩小\n            \n            val displayWidth = (originalWidthDp * scale).dp\n            val displayHeight = (originalHeightDp * scale).dp\n            \n            logger.debug(\"图片尺寸计算: 原始(${bitmapWidth}x${bitmapHeight}) -> 显示(${displayWidth.value}dp x ${displayHeight.value}dp)\")\n            \n            displayWidth to displayHeight\n        } else {\n            logger.warn(\"图片为空或尺寸无效，返回默认尺寸\")\n            0.dp to 0.dp\n        }\n    }\n    \n    /**\n     * 获取图片的像素尺寸\n     * @param state 应用状态\n     * @return 图片的宽度和高度（像素）\n     */\n    fun getImagePixelSize(state: ApplicationState): Pair<Int, Int>? {\n        val image = state.currentImage?.toComposeImageBitmap()\n        return if (image != null && image.width > 0 && image.height > 0) {\n            image.width to image.height\n        } else {\n            logger.warn(\"无法获取有效的图片像素尺寸\")\n            null\n        }\n    }\n    \n    /**\n     * 获取图片的显示尺寸（像素）- 非Composable版本\n     * @param state 应用状态\n     * @param density 密度值\n     * @return 图片的显示宽度和高度（像素）\n     */\n    fun getImageDisplayPixelSize(state: ApplicationState, density: Float): Pair<Int, Int>? {\n        val image = state.currentImage?.toComposeImageBitmap()\n        \n        return if (image != null && image.width > 0 && image.height > 0) {\n            val bitmapWidth = image.width\n            val bitmapHeight = image.height\n            \n            // 原始图片尺寸（dp）\n            val originalWidthDp = bitmapWidth / density\n            val originalHeightDp = bitmapHeight / density\n            \n            // 计算缩放比例，保持宽高比\n            val scale = minOf(\n                DEFAULT_MAX_WIDTH_DP / originalWidthDp,\n                DEFAULT_MAX_HEIGHT_DP / originalHeightDp\n            ).coerceAtMost(1f) // 不放大图片，只缩小\n            \n            val displayWidthPx = (originalWidthDp * scale * density).toInt()\n            val displayHeightPx = (originalHeightDp * scale * density).toInt()\n            \n            displayWidthPx to displayHeightPx\n        } else {\n            null\n        }\n    }\n}\n"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/widget/image/ImageWithConstraints.kt",
    "content": "package cn.netdiscovery.monica.ui.widget.image\n\nimport androidx.compose.foundation.Canvas\nimport androidx.compose.foundation.layout.BoxWithConstraints\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clipToBounds\nimport androidx.compose.ui.geometry.Size\nimport androidx.compose.ui.graphics.ColorFilter\nimport androidx.compose.ui.graphics.DefaultAlpha\nimport androidx.compose.ui.graphics.FilterQuality\nimport androidx.compose.ui.graphics.ImageBitmap\nimport androidx.compose.ui.graphics.drawscope.DrawScope\nimport androidx.compose.ui.graphics.drawscope.translate\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.semantics.Role\nimport androidx.compose.ui.semantics.contentDescription\nimport androidx.compose.ui.semantics.role\nimport androidx.compose.ui.semantics.semantics\nimport androidx.compose.ui.unit.Constraints\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.IntRect\nimport androidx.compose.ui.unit.IntSize\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.image.ImageWithConstraints\n * @author: Tony Shen\n * @date: 2024/5/14 15:45\n * @version: V1.0 <描述当前版本功能>\n */\n@Composable\nfun ImageWithConstraints(\n    modifier: Modifier = Modifier,\n    imageBitmap: ImageBitmap,\n    alignment: Alignment = Alignment.Center,\n    contentScale: ContentScale = ContentScale.Fit,\n    contentDescription: String? = null,\n    alpha: Float = DefaultAlpha,\n    colorFilter: ColorFilter? = null,\n    filterQuality: FilterQuality = DrawScope.DefaultFilterQuality,\n    drawImage: Boolean = true,\n    content: @Composable ImageScope.() -> Unit = {}\n) {\n\n    val semantics = if (contentDescription != null) {\n        Modifier.semantics {\n            this.contentDescription = contentDescription\n            this.role = Role.Image\n        }\n    } else {\n        Modifier\n    }\n\n    BoxWithConstraints(\n        modifier = modifier\n            .then(semantics),\n        contentAlignment = alignment,\n    ) {\n\n        val bitmapWidth = imageBitmap.width\n        val bitmapHeight = imageBitmap.height\n\n        val (boxWidth: Int, boxHeight: Int) = getParentSize(bitmapWidth, bitmapHeight)\n\n        // Src is Bitmap, Dst is the container(Image) that Bitmap will be displayed\n        val srcSize = Size(bitmapWidth.toFloat(), bitmapHeight.toFloat())\n        val dstSize = Size(boxWidth.toFloat(), boxHeight.toFloat())\n\n        val scaleFactor = contentScale.computeScaleFactor(srcSize, dstSize)\n\n        // Image is the container for bitmap that is located inside Box\n        // image bounds can be smaller or bigger than its parent based on how it's scaled\n        val imageWidth = bitmapWidth * scaleFactor.scaleX\n        val imageHeight = bitmapHeight * scaleFactor.scaleY\n\n        val bitmapRect = getScaledBitmapRect(\n            boxWidth = boxWidth,\n            boxHeight = boxHeight,\n            imageWidth = imageWidth,\n            imageHeight = imageHeight,\n            bitmapWidth = bitmapWidth,\n            bitmapHeight = bitmapHeight\n        )\n\n        ImageLayout(\n            constraints = constraints,\n            imageBitmap = imageBitmap,\n            bitmapRect = bitmapRect,\n            imageWidth = imageWidth,\n            imageHeight = imageHeight,\n            boxWidth = boxWidth,\n            boxHeight = boxHeight,\n            alpha = alpha,\n            colorFilter = colorFilter,\n            filterQuality = filterQuality,\n            drawImage = drawImage,\n            content = content\n        )\n    }\n}\n\n@Composable\nprivate fun ImageLayout(\n    constraints: Constraints,\n    imageBitmap: ImageBitmap,\n    bitmapRect: IntRect,\n    imageWidth: Float,\n    imageHeight: Float,\n    boxWidth: Int,\n    boxHeight: Int,\n    alpha: Float = DefaultAlpha,\n    colorFilter: ColorFilter? = null,\n    filterQuality: FilterQuality = DrawScope.DefaultFilterQuality,\n    drawImage: Boolean = true,\n    content: @Composable ImageScope.() -> Unit\n) {\n    val density = LocalDensity.current\n\n    // Dimensions of canvas that will draw this Bitmap\n    val canvasWidthInDp: Dp\n    val canvasHeightInDp: Dp\n\n    with(density) {\n        canvasWidthInDp = imageWidth.coerceAtMost(boxWidth.toFloat()).toDp()\n        canvasHeightInDp = imageHeight.coerceAtMost(boxHeight.toFloat()).toDp()\n    }\n\n    // Send rectangle of Bitmap drawn to Canvas as bitmapRect, content scale modes like\n    // crop might crop image from center so Rect can be such as IntRect(250,250,500,500)\n\n    // canvasWidthInDp, and  canvasHeightInDp are Canvas dimensions coerced to Box size\n    // that covers Canvas\n    val imageScopeImpl = ImageScopeImpl(\n        density = density,\n        constraints = constraints,\n        imageWidth = canvasWidthInDp,\n        imageHeight = canvasHeightInDp,\n        rect = bitmapRect\n    )\n\n    // width and height params for translating draw position if scaled Image dimensions are\n    // bigger than Canvas dimensions\n    if (drawImage) {\n        ImageImpl(\n            modifier = Modifier.size(canvasWidthInDp, canvasHeightInDp),\n            imageBitmap = imageBitmap,\n            alpha = alpha,\n            width = imageWidth.toInt(),\n            height = imageHeight.toInt(),\n            colorFilter = colorFilter,\n            filterQuality = filterQuality\n        )\n    }\n\n    imageScopeImpl.content()\n}\n\n@Composable\nprivate fun ImageImpl(\n    modifier: Modifier,\n    imageBitmap: ImageBitmap,\n    width: Int,\n    height: Int,\n    alpha: Float = DefaultAlpha,\n    colorFilter: ColorFilter? = null,\n    filterQuality: FilterQuality = DrawScope.DefaultFilterQuality\n) {\n    val bitmapWidth = imageBitmap.width\n    val bitmapHeight = imageBitmap.height\n\n    Canvas(modifier = modifier.clipToBounds()) {\n\n        val canvasWidth = size.width.toInt()\n        val canvasHeight = size.height.toInt()\n\n        // Translate to left or down when Image size is bigger than this canvas.\n        // ImageSize is bigger when scale modes like Crop is used which enlarges image\n        // For instance 1000x1000 image can be 1000x2000 for a Canvas with 1000x1000\n        // so top is translated -500 to draw center of ImageBitmap\n        translate(\n            top = (-height + canvasHeight) / 2f,\n            left = (-width + canvasWidth) / 2f\n            ) {\n            drawImage(\n                imageBitmap,\n                srcSize = IntSize(bitmapWidth, bitmapHeight),\n                dstSize = IntSize(width, height),\n                alpha = alpha,\n                colorFilter = colorFilter,\n                filterQuality = filterQuality\n            )\n        }\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/widget/image/ImageWithThumbnail.kt",
    "content": "package cn.netdiscovery.monica.ui.widget.image\n\nimport androidx.compose.foundation.Canvas\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.BoxWithConstraints\nimport androidx.compose.foundation.layout.offset\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.shadow\nimport androidx.compose.ui.geometry.*\nimport androidx.compose.ui.graphics.*\nimport androidx.compose.ui.graphics.drawscope.DrawScope\nimport androidx.compose.ui.input.pointer.PointerInputChange\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.unit.*\nimport cn.netdiscovery.monica.ui.widget.image.gesture.pointerMotionEvents\nimport kotlin.math.roundToInt\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.widget.image.ImageWithThumbnail\n * @author: Tony Shen\n * @date: 2024/6/13 21:52\n * @version: V1.0 <描述当前版本功能>\n */\n\n@Composable\nfun ImageWithThumbnail(\n    modifier: Modifier = Modifier,\n    imageBitmap: ImageBitmap,\n    contentScale: ContentScale = ContentScale.Fit,\n    alignment: Alignment = Alignment.Center,\n    contentDescription: String?,\n    thumbnailState: ThumbnailState = rememberThumbnailState(),\n    alpha: Float = DefaultAlpha,\n    colorFilter: ColorFilter? = null,\n    filterQuality: FilterQuality = DrawScope.DefaultFilterQuality,\n    drawOriginalImage: Boolean = true,\n    onDown: ((Offset) -> Unit)? = null,\n    onMove: ((Offset) -> Unit)? = null,\n    onUp: (() -> Unit)? = null,\n    onThumbnailCenterChange: ((Offset) -> Unit)? = null,\n    content: @Composable ImageScope.() -> Unit = {}\n) {\n\n    ImageWithConstraints(\n        modifier = modifier,\n        contentScale = contentScale,\n        alignment = alignment,\n        contentDescription = contentDescription,\n        alpha = alpha,\n        colorFilter = colorFilter,\n        filterQuality = filterQuality,\n        imageBitmap = imageBitmap,\n        drawImage = drawOriginalImage\n    ) {\n\n        val imageScope = this\n        val density = LocalDensity.current\n\n        val scaledImageBitmap = getScaledImageBitmap(imageBitmap, contentScale)\n\n        val size = rememberUpdatedState(\n            newValue = Size(\n                width = imageWidth.value * density.density,\n                height = imageHeight.value * density.density\n            )\n        )\n\n        var offset by remember(key1 = contentScale, key2 = scaledImageBitmap) {\n            mutableStateOf(\n                Offset.Unspecified\n            )\n        }\n\n        fun updateOffset(pointerInputChange: PointerInputChange): Offset {\n            val offsetX = pointerInputChange.position.x\n                .coerceIn(0f, size.value.width)\n            val offsetY = pointerInputChange.position.y\n                .coerceIn(0f, size.value.height)\n            pointerInputChange.consume()\n            return Offset(offsetX, offsetY)\n        }\n\n        val thumbnailModifier = Modifier\n            .pointerMotionEvents(\n                key1 = contentScale,\n                key2 = scaledImageBitmap,\n                onDown = { pointerInputChange: PointerInputChange ->\n                    offset = updateOffset(pointerInputChange)\n                    onDown?.invoke(offset)\n                },\n                onMove = { pointerInputChange: PointerInputChange ->\n                    offset = updateOffset(pointerInputChange)\n                    onMove?.invoke(offset)\n                },\n                onUp = {\n                    onUp?.invoke()\n                }\n            )\n\n        ThumbnailLayout(\n            modifier = thumbnailModifier.size(this.imageWidth, this.imageHeight),\n            imageBitmap = scaledImageBitmap,\n            thumbnailState = thumbnailState,\n            offset = offset,\n            onThumbnailCenterChange = onThumbnailCenterChange\n        )\n\n        Box(\n            modifier = Modifier\n                .size(this.imageWidth, this.imageHeight),\n        ) {\n            imageScope.content()\n        }\n    }\n}\n\n/**\n * [ThumbnailLayout] displays thumbnail of bitmap it draws in corner specified\n * by [ThumbnailState.position]. When touch position is close to thumbnail\n * position if [ThumbnailState.dynamicPosition]\n * is set to true moves thumbnail to corner specified by [ThumbnailState.moveTo]\n *\n * @param imageBitmap The [ImageBitmap] to draw\n * into the destination. The default is [FilterQuality.Low] which scales using a bilinear\n * sampling algorithm\n *\n * @param onThumbnailCenterChange callback to get center of thumbnail\n */\n@Composable\nprivate fun ThumbnailLayout(\n    modifier: Modifier,\n    imageBitmap: ImageBitmap,\n    thumbnailState: ThumbnailState,\n    alpha: Float = 1.0f,\n    colorFilter: ColorFilter? = null,\n    filterQuality: FilterQuality = DrawScope.DefaultFilterQuality,\n    offset: Offset,\n    onThumbnailCenterChange: ((Offset) -> Unit)? = null\n) {\n    ThumbnailLayoutImpl(\n        modifier = modifier,\n        imageBitmap = imageBitmap,\n        thumbnailState = thumbnailState,\n        offset = offset,\n        alpha = alpha,\n        colorFilter = colorFilter,\n        filterQuality = filterQuality,\n        onThumbnailCenterChange = onThumbnailCenterChange\n    )\n}\n\n@Composable\nprivate fun ThumbnailLayoutImpl(\n    modifier: Modifier,\n    imageBitmap: ImageBitmap,\n    thumbnailState: ThumbnailState,\n    offset: Offset,\n    alpha: Float = 1.0f,\n    colorFilter: ColorFilter? = null,\n    filterQuality: FilterQuality = DrawScope.DefaultFilterQuality,\n    onThumbnailCenterChange: ((Offset) -> Unit)? = null\n) {\n    val thumbnailSize = thumbnailState.size\n    val thumbnailPosition = thumbnailState.position\n    val dynamicPosition = thumbnailState.dynamicPosition\n    val moveTo = thumbnailState.moveTo\n    val thumbnailZoom = thumbnailState.thumbnailZoom\n\n    BoxWithConstraints(modifier) {\n\n        val canvasWidth = constraints.maxWidth.toFloat()\n        val canvasHeight = constraints.maxHeight.toFloat()\n\n        val thumbnailWidthInPx: Float\n        val thumbnailHeightInPx: Float\n\n\n        with(LocalDensity.current) {\n            thumbnailWidthInPx = thumbnailSize.width.toPx()\n            thumbnailHeightInPx = thumbnailSize.height.toPx()\n        }\n\n        // Get thumbnail size as parameter but limit max size to minimum of canvasWidth and Height\n        val imageThumbnailWidth: Int = thumbnailWidthInPx.coerceAtMost(canvasWidth).roundToInt()\n        val imageThumbnailHeight: Int = thumbnailHeightInPx.coerceAtMost(canvasHeight).roundToInt()\n\n        val thumbnailOffset = getThumbnailPositionOffset(\n            offset = offset,\n            canvasWidth = canvasWidth,\n            canvasHeight = canvasHeight,\n            imageThumbnailWidth = imageThumbnailWidth,\n            imageThumbnailHeight = imageThumbnailHeight,\n            thumbnailPosition = thumbnailPosition,\n            dynamicPosition = dynamicPosition,\n            moveTo = moveTo\n        )\n\n        // Center of  thumbnail\n        val centerX: Float = thumbnailOffset.x + imageThumbnailWidth / 2f\n        val centerY: Float = thumbnailOffset.y + imageThumbnailHeight / 2f\n        onThumbnailCenterChange?.invoke(Offset(centerX, centerY))\n\n        Canvas(modifier = Modifier\n            .offset {\n                thumbnailOffset\n            }\n            .then(\n                thumbnailState.shadow?.let { shadow: MaterialShadow ->\n                    Modifier.shadow(\n                        elevation = shadow.elevation,\n                        shape = thumbnailState.shape,\n                        ambientColor = shadow.ambientShadowColor,\n                        spotColor = shadow.spotColor\n                    )\n                } ?: Modifier\n            )\n            .then(\n                thumbnailState.border?.let { border: Border ->\n                    Modifier.border(\n                        width = border.strokeWidth,\n                        shape = thumbnailState.shape,\n                        brush = border.color\n                    )\n                } ?: Modifier\n            )\n            .size(thumbnailSize)\n        ) {\n\n            val zoom = thumbnailZoom.coerceAtLeast(100)\n            val zoomScale = zoom / 100f\n\n            val srcOffset = if (offset.isSpecified && offset.isFinite) {\n                getSrcOffset(\n                    offset = offset,\n                    imageBitmap = imageBitmap,\n                    zoomScale = zoomScale,\n                    size = Size(canvasWidth, canvasHeight),\n                    imageThumbnailSize = imageThumbnailWidth\n                )\n            } else {\n                IntOffset.Zero\n            }\n\n            drawImage(\n                image = imageBitmap,\n                srcOffset = srcOffset,\n                srcSize = IntSize(\n                    width = (imageThumbnailWidth / zoomScale).toInt(),\n                    height = (imageThumbnailWidth / zoomScale).toInt()\n                ),\n                dstSize = IntSize(imageThumbnailWidth, imageThumbnailWidth),\n                alpha = alpha,\n                colorFilter = colorFilter,\n                filterQuality = filterQuality,\n            )\n        }\n\n    }\n}\n\nprivate fun getThumbnailPositionOffset(\n    offset: Offset,\n    canvasWidth: Float,\n    canvasHeight: Float,\n    thumbnailPosition: ThumbnailPosition = ThumbnailPosition.TopLeft,\n    dynamicPosition: Boolean = true,\n    moveTo: ThumbnailPosition = ThumbnailPosition.TopRight,\n    imageThumbnailWidth: Int,\n    imageThumbnailHeight: Int\n): IntOffset {\n\n    val thumbnailOffset = calculateThumbnailOffset(\n        thumbnailPosition,\n        canvasWidth,\n        canvasHeight,\n        imageThumbnailWidth,\n        imageThumbnailHeight\n    )\n\n    if (offset.isUnspecified || !offset.isFinite) return thumbnailOffset\n    if (!dynamicPosition || thumbnailPosition == moveTo) return thumbnailOffset\n\n    val offsetX = offset.x\n        .coerceIn(0f, canvasWidth)\n    val offsetY = offset.y\n        .coerceIn(0f, canvasHeight)\n\n    // Calculate distance from touch position to center of thumbnail\n    val x = offsetX - (thumbnailOffset.x + imageThumbnailWidth / 2)\n    val y = offsetY - (thumbnailOffset.y + imageThumbnailHeight / 2)\n    val distanceToThumbnailCenter = (x * x + y * y)\n\n    // pointer position is in bounds of thumbnail, calculate alternative position to move to\n    return if (distanceToThumbnailCenter < imageThumbnailWidth * imageThumbnailHeight) {\n        calculateThumbnailOffset(\n            moveTo,\n            canvasWidth,\n            canvasHeight,\n            imageThumbnailWidth,\n            imageThumbnailHeight\n        )\n    } else {\n        thumbnailOffset\n    }\n}\n\n/**\n * Calculate thumbnail position based on which corner it's in\n */\nprivate fun calculateThumbnailOffset(\n    thumbnailPosition: ThumbnailPosition,\n    canvasWidth: Float,\n    canvasHeight: Float,\n    imageThumbnailWidth: Int,\n    imageThumbnailHeight: Int\n): IntOffset {\n    return when (thumbnailPosition) {\n        ThumbnailPosition.TopLeft -> {\n            IntOffset(x = 0, y = 0)\n        }\n        ThumbnailPosition.TopRight -> {\n            IntOffset(x = (canvasWidth - imageThumbnailWidth).toInt(), y = 0)\n        }\n\n        ThumbnailPosition.BottomLeft -> {\n            IntOffset(x = 0, y = (canvasHeight - imageThumbnailHeight).toInt())\n        }\n\n        ThumbnailPosition.BottomRight -> {\n            IntOffset(\n                x = (canvasWidth - imageThumbnailWidth).toInt(),\n                y = (canvasHeight - imageThumbnailHeight).toInt()\n            )\n        }\n    }\n}\n\n/**\n * Get offset for Src. Src is the bitmap that will be drawn to canvas. Based on it's\n * size and offset any section or whole bitmap can be drawn.\n * Setting positive offset on x axis moves visible section of bitmap to the left.\n * @param offset pointer touch position\n * @param imageBitmap is image that will be drawn\n * @param zoomScale scale of zoom between [1f-5f]\n */\nprivate fun getSrcOffset(\n    offset: Offset,\n    imageBitmap: ImageBitmap,\n    zoomScale: Float,\n    size: Size,\n    imageThumbnailSize: Int\n): IntOffset {\n\n    val canvasWidth = size.width\n    val canvasHeight = size.height\n\n    val bitmapWidth = imageBitmap.width\n    val bitmapHeight = imageBitmap.height\n\n    val offsetX = offset.x\n        .coerceIn(0f, canvasWidth)\n    val offsetY = offset.y\n        .coerceIn(0f, canvasHeight)\n\n    // Setting offset for src moves the position in Bitmap\n    // Bitmap is SRC while where we draw is DST.\n    // Setting offset of dst moves where we draw in Canvas\n    // Setting src moves to which part of the bitmap we draw\n    // Coercing at right bound (bitmap.width - imageThumbnailSize) lets to limit offset\n    // to thumbnailSize when user moves pointer to right.\n    // If image has 100px width and thumbnail 10 when user moves to 95 we see a width with 5px\n    // coercing lets you keep 10px all the time\n    val srcOffsetX =\n        (offsetX * bitmapWidth / canvasWidth - imageThumbnailSize / zoomScale / 2)\n            .coerceIn(0f, bitmapWidth - imageThumbnailSize / zoomScale)\n    val srcOffsetY =\n        (offsetY * bitmapHeight / canvasHeight - imageThumbnailSize / zoomScale / 2)\n            .coerceIn(0f, bitmapHeight - imageThumbnailSize / zoomScale)\n\n    return IntOffset(srcOffsetX.toInt(), srcOffsetY.toInt())\n}\n\nenum class ThumbnailPosition {\n    TopLeft, TopRight, BottomLeft, BottomRight\n}\n\n/**\n * Creates and stores UI properties for [ImageWithThumbnail].\n * @param size size of the thumbnail\n * @param position position of the thumbnail. It's top left corner by default\n * @param dynamicPosition flag that changes mobility of thumbnail when user touch is\n * in proximity of the thumbnail\n * @param moveTo corner to move thumbnail if user touch is in proximity of the thumbnail. By default\n * it's top right corner.\n * @param thumbnailZoom zoom amount of thumbnail. It's in range of [100-500]. 100 corresponds\n * @param shape of the thumbnail\n * @param shadow if not null draws shadow behind thumbnail with given [shape]\n * @param border if not null draws border around thumbnail with given [shape]\n */\n@Composable\nfun rememberThumbnailState(\n    size: DpSize = DpSize(80.dp, 80.dp),\n    position: ThumbnailPosition = ThumbnailPosition.TopLeft,\n    dynamicPosition: Boolean = true,\n    moveTo: ThumbnailPosition = ThumbnailPosition.TopRight,\n    thumbnailZoom: Int = 200,\n    shape: Shape = RoundedCornerShape(8.dp),\n    shadow: MaterialShadow = MaterialShadow(\n        elevation = 2.dp,\n        ambientShadowColor = DefaultShadowColor,\n        spotColor = DefaultShadowColor\n    ),\n    border: Border? = null,\n): ThumbnailState {\n\n    return remember {\n        ThumbnailState(\n            size = size,\n            position = position,\n            dynamicPosition = dynamicPosition,\n            moveTo = moveTo,\n            thumbnailZoom = thumbnailZoom,\n            shape = shape,\n            shadow = shadow,\n            border = border\n        )\n    }\n}\n\n@Immutable\ndata class ThumbnailState internal constructor(\n    @Stable\n    val size: DpSize = DpSize(80.dp, 80.dp),\n    @Stable\n    val position: ThumbnailPosition = ThumbnailPosition.TopLeft,\n    @Stable\n    val dynamicPosition: Boolean = true,\n    @Stable\n    val moveTo: ThumbnailPosition = ThumbnailPosition.TopRight,\n    @Stable\n    val thumbnailZoom: Int = 200,\n    val shape: Shape = RoundedCornerShape(8.dp),\n    @Stable\n    val shadow: MaterialShadow? = MaterialShadow(\n        elevation = 2.dp,\n        ambientShadowColor = DefaultShadowColor,\n        spotColor = DefaultShadowColor\n    ),\n    @Stable\n    val border: Border? = null\n)\n\n@Immutable\ndata class MaterialShadow(\n    @Stable\n    val elevation: Dp = 2.dp,\n    @Stable\n    val ambientShadowColor: Color = DefaultShadowColor,\n    @Stable\n    val spotColor: Color = DefaultShadowColor,\n)\n\n@Composable\nfun Border(\n    color: Color,\n    strokeWidth: Dp,\n): Border {\n    return Border(strokeWidth = strokeWidth, color = SolidColor(color))\n}\n\n@Composable\nfun Border(\n    brush: Brush,\n    strokeWidth: Dp\n): Border {\n    return Border(strokeWidth = strokeWidth, color = brush)\n}\n\n@Immutable\ndata class Border internal constructor(\n    @Stable\n    val strokeWidth: Dp,\n    @Stable\n    val color: Brush\n)\n\nval DefaultShadowColor = Color.Black\n"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/widget/image/gesture/AwaitDragMotionModifier.kt",
    "content": "package cn.netdiscovery.monica.ui.widget.image.gesture\n\nimport androidx.compose.foundation.gestures.awaitFirstDown\nimport androidx.compose.foundation.gestures.awaitTouchSlopOrCancellation\nimport androidx.compose.foundation.gestures.drag\nimport androidx.compose.foundation.gestures.forEachGesture\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.input.pointer.AwaitPointerEventScope\nimport androidx.compose.ui.input.pointer.PointerInputChange\nimport androidx.compose.ui.input.pointer.consumePositionChange\nimport androidx.compose.ui.input.pointer.pointerInput\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.widget.image.gesture.AwaitDragMotionModifier\n * @author: Tony Shen\n * @date: 2024/5/20 19:18\n * @version: V1.0 <描述当前版本功能>\n */\n\nsuspend fun AwaitPointerEventScope.awaitDragMotionEvent(\n    onDragStart: (PointerInputChange) -> Unit = {},\n    onDrag: (PointerInputChange) -> Unit = {},\n    onDragEnd: (PointerInputChange) -> Unit = {}\n) {\n    // Wait for at least one pointer to press down, and set first contact position\n    val down: PointerInputChange = awaitFirstDown()\n    onDragStart(down)\n\n    var pointer = down\n\n    // 🔥 Waits for drag threshold to be passed by pointer\n    // or it returns null if up event is triggered\n    val change: PointerInputChange? =\n        awaitTouchSlopOrCancellation(down.id) { change: PointerInputChange, over: Offset ->\n            // 🔥🔥 If consumePositionChange() is not consumed drag does not\n            // function properly.\n            // Consuming position change causes change.positionChanged() to return false.\n            change.consumePositionChange()\n        }\n\n    if (change != null) {\n        // 🔥 Calls  awaitDragOrCancellation(pointer) in a while loop\n        drag(change.id) { pointerInputChange: PointerInputChange ->\n            pointer = pointerInputChange\n            onDrag(pointer)\n        }\n\n        // All of the pointers are up\n        onDragEnd(pointer)\n    } else {\n        // Drag threshold is not passed and last pointer is up\n        onDragEnd(pointer)\n    }\n}\n\nfun Modifier.dragMotionEvent(\n    onDragStart: (PointerInputChange) -> Unit = {},\n    onDrag: (PointerInputChange) -> Unit = {},\n    onDragEnd: (PointerInputChange) -> Unit = {}\n) = this.then(\n    Modifier.pointerInput(Unit) {\n        forEachGesture {\n            awaitPointerEventScope {\n                awaitDragMotionEvent(onDragStart, onDrag, onDragEnd)\n            }\n        }\n    }\n)"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/widget/image/gesture/AwaitPointerMontionEvent.kt",
    "content": "package cn.netdiscovery.monica.ui.widget.image.gesture\n\nimport androidx.compose.foundation.gestures.awaitEachGesture\nimport androidx.compose.foundation.gestures.awaitFirstDown\nimport androidx.compose.foundation.gestures.forEachGesture\nimport androidx.compose.ui.input.pointer.*\nimport androidx.compose.ui.input.pointer.PointerEventPass.*\nimport kotlinx.coroutines.coroutineScope\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.launch\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.widget.image.gesture.AwaitPointerMontionEvent\n * @author: Tony Shen\n * @date: 2024/5/26 18:59\n * @version: V1.0 <描述当前版本功能>\n */\n\nsuspend fun PointerInputScope.detectMotionEvents(\n    onDown: (PointerInputChange) -> Unit = {},\n    onMove: (PointerInputChange) -> Unit = {},\n    onUp: (PointerInputChange) -> Unit = {},\n    delayAfterDownInMillis: Long = 0L,\n    requireUnconsumed: Boolean = true,\n    pass: PointerEventPass = PointerEventPass.Main\n) {\n\n    coroutineScope {\n        forEachGesture {\n            awaitPointerEventScope {\n                // Wait for at least one pointer to press down, and set first contact position\n                val down: PointerInputChange = awaitFirstDown(requireUnconsumed)\n                onDown(down)\n\n                var pointer = down\n                // Main pointer is the one that is down initially\n                var pointerId = down.id\n\n                // If a move event is followed fast enough down is skipped, especially by Canvas\n                // to prevent it we add delay after first touch\n                var waitedAfterDown = false\n\n                launch {\n                    delay(delayAfterDownInMillis)\n                    waitedAfterDown = true\n                }\n\n                while (true) {\n\n                    val event: PointerEvent = awaitPointerEvent(pass)\n\n                    val anyPressed = event.changes.any { it.pressed }\n\n                    // There are at least one pointer pressed\n                    if (anyPressed) {\n                        // Get pointer that is down, if first pointer is up\n                        // get another and use it if other pointers are also down\n                        // event.changes.first() doesn't return same order\n                        val pointerInputChange =\n                            event.changes.firstOrNull { it.id == pointerId }\n                                ?: event.changes.first()\n\n                        // Next time will check same pointer with this id\n                        pointerId = pointerInputChange.id\n                        pointer = pointerInputChange\n\n                        if (waitedAfterDown) {\n                            onMove(pointer)\n                        }\n                    } else {\n                        // All of the pointers are up\n                        onUp(pointer)\n                        break\n                    }\n                }\n            }\n        }\n    }\n}\n\n/**\n * Reads [awaitFirstDown], and [AwaitPointerEventScope.awaitPointerEvent] to\n * get [PointerInputChange] and motion event states\n * [onDown], [onMove], and [onUp]. Unlike overload of this function [onMove] returns\n * list of [PointerInputChange] to get data about all pointers that are on the screen.\n *\n * To prevent other pointer functions that call [awaitFirstDown]\n * or [AwaitPointerEventScope.awaitPointerEvent]\n * (scroll, swipe, detect functions)\n * receiving changes call [PointerInputChange.consume]  in [onMove]  or call\n * [PointerInputChange.consume] in [onDown] to prevent events\n * that check first pointer interaction.\n *\n * @param onDown is invoked when first pointer is down initially.\n * @param onMove one or multiple pointers are being moved on screen.\n * @param onUp last pointer is up\n * @param delayAfterDownInMillis is optional delay after [onDown] This delay might be\n * required Composables like **Canvas** to process [onDown] before [onMove]\n * @param requireUnconsumed is `true` and the first\n * down is consumed in the [PointerEventPass.Main] pass, that gesture is ignored.\n * @param pass The enumeration of passes where [PointerInputChange]\n * traverses up and down the UI tree.\n *\n * PointerInputChanges traverse throw the hierarchy in the following passes:\n *\n * 1. [Initial]: Down the tree from ancestor to descendant.\n * 2. [Main]: Up the tree from descendant to ancestor.\n * 3. [Final]: Down the tree from ancestor to descendant.\n *\n * These passes serve the following purposes:\n *\n * 1. Initial: Allows ancestors to consume aspects of [PointerInputChange] before descendants.\n * This is where, for example, a scroller may block buttons from getting tapped by other fingers\n * once scrolling has started.\n * 2. Main: The primary pass where gesture filters should react to and consume aspects of\n * [PointerInputChange]s. This is the primary path where descendants will interact with\n * [PointerInputChange]s before parents. This allows for buttons to respond to a tap before a\n * container of the bottom to respond to a tap.\n * 3. Final: This pass is where children can learn what aspects of [PointerInputChange]s were\n * consumed by parents during the [Main] pass. For example, this is how a button determines that\n * it should no longer respond to fingers lifting off of it because a parent scroller has\n * consumed movement in a [PointerInputChange].\n *\n */\nsuspend fun PointerInputScope.detectMotionEventsAsList(\n    onDown: (PointerInputChange) -> Unit = {},\n    onMove: (List<PointerInputChange>) -> Unit = {},\n    onUp: (PointerInputChange) -> Unit = {},\n    delayAfterDownInMillis: Long = 0L,\n    requireUnconsumed: Boolean = true,\n    pass: PointerEventPass = PointerEventPass.Main\n) {\n\n    coroutineScope {\n        awaitEachGesture {\n            // Wait for at least one pointer to press down, and set first contact position\n            val down: PointerInputChange = awaitFirstDown(\n                requireUnconsumed = requireUnconsumed,\n                pass = pass\n            )\n            onDown(down)\n\n            var pointer = down\n            // Main pointer is the one that is down initially\n            var pointerId = down.id\n\n            // If a move event is followed fast enough down is skipped, especially by Canvas\n            // to prevent it we add delay after first touch\n            var waitedAfterDown = false\n\n            launch {\n                delay(delayAfterDownInMillis)\n                waitedAfterDown = true\n            }\n\n            while (true) {\n\n                val event: PointerEvent = awaitPointerEvent(pass)\n\n                val anyPressed = event.changes.any { it.pressed }\n\n                // There are at least one pointer pressed\n                if (anyPressed) {\n                    // Get pointer that is down, if first pointer is up\n                    // get another and use it if other pointers are also down\n                    // event.changes.first() doesn't return same order\n                    val pointerInputChange =\n                        event.changes.firstOrNull { it.id == pointerId }\n                            ?: event.changes.first()\n\n                    // Next time will check same pointer with this id\n                    pointerId = pointerInputChange.id\n                    pointer = pointerInputChange\n\n                    if (waitedAfterDown) {\n                        onMove(event.changes)\n                    }\n\n                } else {\n                    // All of the pointers are up\n                    onUp(pointer)\n                    break\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/widget/image/gesture/MotionEvent.kt",
    "content": "package cn.netdiscovery.monica.ui.widget.image.gesture\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.image.gesture.MotionEvent\n * @author: Tony Shen\n * @date: 2024/5/14 16:00\n * @version: V1.0 <描述当前版本功能>\n */\nenum class MotionEvent {\n    Idle, Down, Move, Up\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/widget/image/gesture/PointerMotionModify.kt",
    "content": "package cn.netdiscovery.monica.ui.widget.image.gesture\n\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.input.pointer.PointerEventPass\nimport androidx.compose.ui.input.pointer.PointerInputChange\nimport androidx.compose.ui.input.pointer.pointerInput\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.widget.image.gesture.PointerMotionModify\n * @author: Tony Shen\n * @date: 2024/6/13 22:00\n * @version: V1.0 <描述当前版本功能>\n */\n\nfun Modifier.pointerMotionEvents(\n    onDown: (PointerInputChange) -> Unit = {},\n    onMove: (PointerInputChange) -> Unit = {},\n    onUp: (PointerInputChange) -> Unit = {},\n    delayAfterDownInMillis: Long = 0L,\n    requireUnconsumed: Boolean = true,\n    pass: PointerEventPass = PointerEventPass.Main,\n    key1: Any?,\n    key2: Any?\n) = this.then(\n    Modifier.pointerInput(key1, key2) {\n        detectMotionEvents(\n            onDown,\n            onMove,\n            onUp,\n            delayAfterDownInMillis,\n            requireUnconsumed,\n            pass\n        )\n    }\n)"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/widget/image/gesture/TransformGestures.kt",
    "content": "package cn.netdiscovery.monica.ui.widget.image.gesture\n\nimport androidx.compose.foundation.gestures.*\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.input.pointer.PointerEventPass\nimport androidx.compose.ui.input.pointer.PointerInputChange\nimport androidx.compose.ui.input.pointer.PointerInputScope\nimport androidx.compose.ui.input.pointer.positionChanged\nimport kotlin.math.PI\nimport kotlin.math.abs\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.widget.image.gesture.TransformGestures\n * @author: Tony Shen\n * @date: 2024/5/26 15:36\n * @version: V1.0 <描述当前版本功能>\n */\n\nsuspend fun PointerInputScope.detectTransformGestures(\n    panZoomLock: Boolean = false,\n    consume: Boolean = true,\n    pass: PointerEventPass = PointerEventPass.Main,\n    onGestureStart: (PointerInputChange) -> Unit = {},\n    onGesture: (\n        centroid: Offset,\n        pan: Offset,\n        zoom: Float,\n        rotation: Float,\n        mainPointer: PointerInputChange,\n        changes: List<PointerInputChange>\n    ) -> Unit,\n    onGestureEnd: (PointerInputChange) -> Unit = {}\n) {\n    awaitEachGesture {\n        var rotation = 0f\n        var zoom = 1f\n        var pan = Offset.Zero\n        var pastTouchSlop = false\n        val touchSlop = viewConfiguration.touchSlop\n        var lockedToPanZoom = false\n\n        // Wait for at least one pointer to press down, and set first contact position\n        val down: PointerInputChange = awaitFirstDown(\n            requireUnconsumed = false,\n            pass = pass\n        )\n        onGestureStart(down)\n\n        var pointer = down\n        // Main pointer is the one that is down initially\n        var pointerId = down.id\n\n        do {\n            val event = awaitPointerEvent(pass = pass)\n\n            // If any position change is consumed from another PointerInputChange\n            // or pointer count requirement is not fulfilled\n            val canceled =\n                event.changes.any { it.isConsumed }\n\n            if (!canceled) {\n\n                // Get pointer that is down, if first pointer is up\n                // get another and use it if other pointers are also down\n                // event.changes.first() doesn't return same order\n                val pointerInputChange =\n                    event.changes.firstOrNull { it.id == pointerId }\n                        ?: event.changes.first()\n\n                // Next time will check same pointer with this id\n                pointerId = pointerInputChange.id\n                pointer = pointerInputChange\n\n                val zoomChange = event.calculateZoom()\n                val rotationChange = event.calculateRotation()\n                val panChange = event.calculatePan()\n\n                if (!pastTouchSlop) {\n                    zoom *= zoomChange\n                    rotation += rotationChange\n                    pan += panChange\n\n                    val centroidSize = event.calculateCentroidSize(useCurrent = false)\n                    val zoomMotion = abs(1 - zoom) * centroidSize\n                    val rotationMotion =\n                        abs(rotation * PI.toFloat() * centroidSize / 180f)\n                    val panMotion = pan.getDistance()\n\n                    if (zoomMotion > touchSlop ||\n                        rotationMotion > touchSlop ||\n                        panMotion > touchSlop\n                    ) {\n                        pastTouchSlop = true\n                        lockedToPanZoom = panZoomLock && rotationMotion < touchSlop\n                    }\n                }\n\n                if (pastTouchSlop) {\n                    val centroid = event.calculateCentroid(useCurrent = false)\n                    val effectiveRotation = if (lockedToPanZoom) 0f else rotationChange\n                    if (effectiveRotation != 0f ||\n                        zoomChange != 1f ||\n                        panChange != Offset.Zero\n                    ) {\n                        onGesture(\n                            centroid,\n                            panChange,\n                            zoomChange,\n                            effectiveRotation,\n                            pointer,\n                            event.changes\n                        )\n                    }\n\n                    if (consume) {\n                        event.changes.forEach {\n                            if (it.positionChanged()) {\n                                it.consume()\n                            }\n                        }\n                    }\n                }\n            }\n        } while (!canceled && event.changes.any { it.pressed })\n        onGestureEnd(pointer)\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/ui/widget/properties/ExposedSelectionMenu.kt",
    "content": "package cn.netdiscovery.monica.ui.widget.properties\n\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material.*\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.unit.dp\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.ui.widget.properties.ExposedSelectionMenu\n * @author: Tony Shen\n * @date: 2024/11/26 13:08\n * @version: V1.0 <描述当前版本功能>\n */\n\n/**\n * Expandable selection menu\n * @param title of the displayed item on top\n * @param index index of selected item\n * @param options list of [String] options\n * @param onSelected lambda to be invoked when an item is selected that returns\n * its index.\n */\n@OptIn(ExperimentalMaterialApi::class)\n@Composable\nfun ExposedSelectionMenu(\n    title: String,\n    index: Int,\n    options: List<String>,\n    onSelected: (Int) -> Unit\n) {\n\n    var expanded by remember { mutableStateOf(false) }\n    var selectedOptionText by remember { mutableStateOf(options[index]) }\n    var selectedIndex = remember { index }\n\n    ExposedDropdownMenuBox(\n        modifier = Modifier\n            .fillMaxWidth()\n            .padding(vertical = 4.dp),\n        expanded = expanded,\n        onExpandedChange = {\n            expanded = !expanded\n        }\n    ) {\n        TextField(\n            modifier = Modifier.fillMaxWidth(),\n            readOnly = true,\n            value = selectedOptionText,\n            onValueChange = { },\n            label = { Text(title) },\n            trailingIcon = {\n                ExposedDropdownMenuDefaults.TrailingIcon(\n                    expanded = expanded\n                )\n            },\n            colors = ExposedDropdownMenuDefaults.textFieldColors(\n                backgroundColor = Color.White,\n                focusedIndicatorColor = Color.Transparent,\n                unfocusedIndicatorColor = Color.Transparent,\n                disabledIndicatorColor = Color.Transparent,\n            )\n        )\n        ExposedDropdownMenu(\n            modifier = Modifier.fillMaxWidth(),\n            expanded = expanded,\n            onDismissRequest = {\n                expanded = false\n\n            }\n        ) {\n            options.forEachIndexed { index: Int, selectionOption: String ->\n                DropdownMenuItem(\n                    modifier = Modifier.fillMaxWidth(),\n                    onClick = {\n                        selectedOptionText = selectionOption\n                        expanded = false\n                        selectedIndex = index\n                        onSelected(selectedIndex)\n                    }\n                ) {\n                    Text(text = selectionOption)\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/utils/AppDirs.kt",
    "content": "package cn.netdiscovery.monica.utils\n\nimport Monica.config.BuildConfig\nimport cn.netdiscovery.monica.config.*\nimport java.io.File\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.utils.AppDirs\n * @author: Tony Shen\n * @date:  2025/6/2 16:08\n * @version: V1.0 <描述当前版本功能>\n */\nobject AppDirs {\n\n    private const val appName = \"Monica\"\n\n    val cacheDir: File by lazy {\n        val path = when {\n            isMac -> {\n                if (BuildConfig.IS_PRO_VERSION) {\n                    \"$userHome/Library/Caches/$appName/rxcache\"\n                } else {\n                    \"$workDirectory/rxcache\"\n                }\n            }\n\n            isLinux -> {\n                if (BuildConfig.IS_PRO_VERSION) {\n                    \"$userHome/.cache/$appName\"\n                } else {\n                    \"$workDirectory/rxcache\"\n                }\n            }\n\n            isWindows -> {\n                if (BuildConfig.IS_PRO_VERSION) {\n                    \"${getWindowsAppData()}/$appName/Cache\"\n                } else {\n                    \"$workDirectory/rxcache\"\n                }\n            }\n\n            else -> \"$userHome/.cache/$appName\"\n        }\n        createDir(path)\n    }\n\n    val logDir: File by lazy {\n        val path = when {\n            isMac -> {\n                if (BuildConfig.IS_PRO_VERSION) {\n                    \"$userHome/Library/Logs/$appName\"\n                } else {\n                    \"$workDirectory/log\"\n                }\n            }\n\n            isLinux -> {\n                if (BuildConfig.IS_PRO_VERSION) {\n                    \"$userHome/.local/share/$appName/logs\"\n                } else {\n                    \"$workDirectory/log\"\n                }\n            }\n\n            isWindows -> {\n                if (BuildConfig.IS_PRO_VERSION) {\n                    \"${getWindowsAppData()}/$appName/Logs\"\n                } else {\n                    \"$workDirectory/log\"\n                }\n            }\n\n            else -> \"$userHome/.local/share/$appName/logs\"\n        }\n        createDir(path)\n    }\n\n    private fun getWindowsAppData(): String {\n        return System.getenv(\"APPDATA\") ?: \"$userHome/AppData/Roaming\"\n    }\n\n    private fun createDir(path: String): File {\n        val file = File(path)\n        if (!file.exists()) {\n            file.mkdirs()\n        }\n        return file\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/utils/ButtonUtils.kt",
    "content": "package cn.netdiscovery.monica.utils\n\nimport loadingDisplay\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.utils.ButtonUtils\n * @author: Tony Shen\n * @date: 2024/4/27 17:16\n * @version: V1.0 <描述当前版本功能>\n */\n\n/**\n * 点击按钮后，会带有 loading 的效果\n */\nfun loadingDisplay(block: Action) {\n    loadingDisplay = true\n    block.invoke()\n    loadingDisplay = false\n}\n\n/**\n * 点击按钮后，会带有 loading 的效果\n */\nsuspend fun loadingDisplayWithSuspend(block:suspend ()->Unit) {\n    loadingDisplay = true\n    block.invoke()\n    loadingDisplay = false\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/utils/DebugUtils.kt",
    "content": "package cn.netdiscovery.monica.utils\n\nimport kotlin.system.measureTimeMillis\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.utils.DebugUtils\n * @author: Tony Shen\n * @date: 2024/4/30 12:43\n * @version: V1.0 调试时，使用的工具类\n */\n\n\n/**\n * 统计耗时任务的时间，便于调试时使用\n */\nfun measure(block: () -> Unit):Long {\n\n    val timeCost = measureTimeMillis {\n        block.invoke()\n    }\n\n    return timeCost\n}\n\n/**\n * 统计耗时任务的时间，便于调试时使用\n */\nsuspend fun measureWithSuspend(block: suspend() -> Unit):Long {\n\n    val timeCost = measureTimeMillis {\n        block.invoke()\n    }\n\n    return timeCost\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/utils/FileChoose.kt",
    "content": "package cn.netdiscovery.monica.utils\n\nimport androidx.compose.ui.awt.ComposeWindow\nimport cn.netdiscovery.monica.state.ApplicationState\nimport cn.netdiscovery.monica.utils.extensions.launchWithLoading\nimport cn.netdiscovery.monica.i18n.LocalizationManager\nimport org.slf4j.Logger\nimport org.slf4j.LoggerFactory\nimport java.awt.datatransfer.DataFlavor\nimport java.awt.dnd.DnDConstants\nimport java.awt.dnd.DropTarget\nimport java.awt.dnd.DropTargetDropEvent\nimport java.io.File\nimport javax.swing.JFileChooser\nimport javax.swing.SwingUtilities\nimport javax.swing.UIManager\nimport javax.swing.filechooser.FileNameExtensionFilter\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.utils.FileChoose\n * @author: Tony Shen\n * @date: 2024/4/26 10:57\n * @version: V1.0 <描述当前版本功能>\n */\nprivate val logger: Logger = LoggerFactory.getLogger(object : Any() {}.javaClass.enclosingClass)\n\nval legalSuffixList: Array<String> = arrayOf(\"jpg\", \"jpeg\", \"png\",\"webp\",\"svg\", \"hdr\", \"CR2\", \"CR3\", \"HEIC\")\n\nfun chooseImage(state: ApplicationState, block:(file: File)->Unit) {\n    showFileSelector(\n        isMultiSelection = false,\n        selectionMode = JFileChooser.FILES_ONLY,\n        onFileSelected = {\n            state.scope.launchWithLoading {\n                val file = it.getOrNull(0)\n                if (file != null) {\n                    logger.info(\"load file: ${file.absolutePath}\")\n                    block.invoke(file)\n                }\n            }\n        }\n    )\n}\n\nprivate fun showFileSelector(\n    suffixList: Array<String> = legalSuffixList,\n    isMultiSelection: Boolean = true,\n    selectionMode: Int = JFileChooser.FILES_AND_DIRECTORIES, // 可以选择目录和文件\n    selectionFileFilter: FileNameExtensionFilter? = FileNameExtensionFilter(\"图片(${legalSuffixList.contentToString()})\", *suffixList), // 文件过滤\n    onFileSelected: (Array<File>) -> Unit\n) {\n    JFileChooser().apply {\n        try {\n            val lookAndFeel = UIManager.getSystemLookAndFeelClassName()\n            UIManager.setLookAndFeel(lookAndFeel)\n            SwingUtilities.updateComponentTreeUI(this)\n        } catch (e: Throwable) {\n            e.printStackTrace()\n        }\n\n        fileSelectionMode = selectionMode\n        isMultiSelectionEnabled = isMultiSelection\n        fileFilter = selectionFileFilter\n\n        val result = showOpenDialog(ComposeWindow())\n        if (result == JFileChooser.APPROVE_OPTION) {\n            if (isMultiSelection) {\n                onFileSelected(this.selectedFiles)\n            }\n            else {\n                val resultArray = arrayOf(this.selectedFile)\n                onFileSelected(resultArray)\n            }\n        }\n    }\n}\n\nfun exportImage(\n    onFileSelected: (JFileChooser) -> Unit\n) {\n    JFileChooser().apply {\n        this.dialogTitle = LocalizationManager.getString(\"export_image\")\n\n        // 添加格式选项\n        val pngFilter = FileNameExtensionFilter(\"PNG 图像 (*.png)\", \"png\")\n        val jpgFilter = FileNameExtensionFilter(\"JPG 图像 (*.jpg)\", \"jpg\")\n        val webpFilter = FileNameExtensionFilter(\"Webp 图像 (*.webp)\", \"webp\")\n        this.addChoosableFileFilter(pngFilter)\n        this.addChoosableFileFilter(jpgFilter)\n        this.addChoosableFileFilter(webpFilter)\n        this.fileFilter = pngFilter // 默认选择 PNG\n\n        val result = this.showSaveDialog(null)\n\n        if (result == JFileChooser.APPROVE_OPTION) {\n            onFileSelected(this)\n        }\n    }\n}\n\nfun dropFileTarget(\n    onFileDrop: (List<String>) -> Unit\n): DropTarget {\n    return object : DropTarget() {\n        override fun drop(event: DropTargetDropEvent) {\n\n            event.acceptDrop(DnDConstants.ACTION_REFERENCE)\n            val dataFlavors = event.transferable.transferDataFlavors\n            dataFlavors.forEach {\n                if (it == DataFlavor.javaFileListFlavor) {\n                    val list = event.transferable.getTransferData(it) as List<*>\n\n                    val pathList = mutableListOf<String>()\n                    list.forEach { filePath ->\n                        pathList.add(filePath.toString())\n                    }\n                    onFileDrop(pathList)\n                }\n            }\n            event.dropComplete(true)\n        }\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/utils/IOUtils.kt",
    "content": "package cn.netdiscovery.monica.utils\n\nimport java.io.Closeable\nimport java.io.IOException\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.utils.IOUtils\n * @author: Tony Shen\n * @date: 2024/5/2 21:47\n * @version: V1.0 <描述当前版本功能>\n */\n\n/**\n * 安全关闭io流\n * @param closeable\n */\nfun closeQuietly(closeable: Closeable?) {\n    if (closeable != null) {\n        try {\n            closeable.close()\n        } catch (e: IOException) {\n            e.printStackTrace()\n        }\n    }\n}\n\n/**\n * 安全关闭io流\n * @param closeables\n */\nfun closeQuietly(vararg closeables: Closeable?) {\n    if (closeables.isNotEmpty()) {\n        for (closeable in closeables) {\n            closeQuietly(closeable)\n        }\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/utils/ImageCompressionUtils.kt",
    "content": "package cn.netdiscovery.monica.utils\n\nimport org.slf4j.Logger\nimport org.slf4j.LoggerFactory\nimport java.awt.Graphics2D\nimport java.awt.RenderingHints\nimport java.awt.image.BufferedImage\nimport java.io.ByteArrayOutputStream\nimport java.io.File\nimport javax.imageio.ImageIO\nimport javax.imageio.ImageWriteParam\nimport javax.imageio.ImageWriter\n\n/**\n * 图像压缩工具类\n * 支持多种压缩算法：JPEG Quality、PNG Optimization、WebP Lossy、WebP Lossless\n * \n * @author: Tony Shen\n * @date: 2025/12/07\n * @version: V1.0\n */\nprivate val logger: Logger = LoggerFactory.getLogger(ImageCompressionUtils::class.java)\n\n/**\n * 压缩算法枚举\n */\nenum class CompressionAlgorithm(val displayName: String, val format: String) {\n    JPEG_QUALITY(\"JPEG Quality\", \"jpg\"),\n    PNG_OPTIMIZATION(\"PNG Optimization\", \"png\"),\n    WEBP_LOSSY(\"WebP Lossy\", \"webp\"),\n    WEBP_LOSSLESS(\"WebP Lossless\", \"webp\")\n}\n\n/**\n * 压缩参数数据类\n */\ndata class CompressionParams(\n    val algorithm: CompressionAlgorithm = CompressionAlgorithm.JPEG_QUALITY,\n    val quality: Float = 0.8f, // 0.0 - 1.0，用于 JPEG 和 WebP Lossy\n    val compressionLevel: Int = 9 // 0 - 9，用于 PNG 和 WebP Lossless\n) {\n    init {\n        require(quality in 0f..1f) { \"Quality must be between 0 and 1\" }\n        require(compressionLevel in 0..9) { \"Compression level must be between 0 and 9\" }\n    }\n}\n\nobject ImageCompressionUtils {\n    \n    /**\n     * 检查系统是否支持 WebP 格式\n     */\n    fun isWebPSupported(): Boolean {\n        return try {\n            val readers = ImageIO.getImageReadersByFormatName(\"webp\")\n            readers.hasNext()\n        } catch (e: Exception) {\n            false\n        }\n    }\n    \n    /**\n     * 获取 WebP 降级后的格式\n     */\n    fun getWebPFallbackFormat(isLossy: Boolean): String {\n        return if (isLossy) \"JPEG\" else \"PNG\"\n    }\n    \n    /**\n     * 压缩单张图片\n     * \n     * @param image BufferedImage 图像对象\n     * @param params 压缩参数\n     * @return 压缩后的字节数组，失败返回 null\n     */\n    /**\n     * 压缩单张图片\n     * \n     * @param image BufferedImage 图像对象\n     * @param params 压缩参数\n     * @return Pair<压缩后的字节数组, 是否使用了降级处理>，失败返回 null\n     */\n    fun compressImage(image: BufferedImage, params: CompressionParams): Pair<ByteArray, Boolean>? {\n        return try {\n            val outputStream = ByteArrayOutputStream()\n            var usedFallback = false\n            \n            when (params.algorithm) {\n                CompressionAlgorithm.JPEG_QUALITY -> {\n                    compressJPEG(image, params.quality, outputStream)\n                }\n                CompressionAlgorithm.PNG_OPTIMIZATION -> {\n                    compressPNG(image, params.compressionLevel, outputStream)\n                }\n                CompressionAlgorithm.WEBP_LOSSY -> {\n                    usedFallback = compressWebP(image, params.quality, true, outputStream)\n                }\n                CompressionAlgorithm.WEBP_LOSSLESS -> {\n                    usedFallback = compressWebP(image, 1f, false, outputStream)\n                }\n            }\n            \n            Pair(outputStream.toByteArray(), usedFallback)\n        } catch (e: Exception) {\n            logger.error(\"Image compression failed\", e)\n            null\n        }\n    }\n    \n    /**\n     * 压缩单张图片（兼容旧接口）\n     */\n    @Deprecated(\"使用 compressImage 替代，可以获取降级信息\")\n    fun compressImageLegacy(image: BufferedImage, params: CompressionParams): ByteArray? {\n        return compressImage(image, params)?.first\n    }\n    \n    /**\n     * 压缩 JPEG 图片\n     */\n    private fun compressJPEG(image: BufferedImage, quality: Float, outputStream: ByteArrayOutputStream) {\n        val writer: ImageWriter? = ImageIO.getImageWritersByFormatName(\"jpg\").next()\n        if (writer != null) {\n            val param = writer.defaultWriteParam\n            var compressionApplied = false\n            if (param.canWriteCompressed()) {\n                try {\n                    param.compressionMode = ImageWriteParam.MODE_EXPLICIT\n                    if (param.compressionTypes.isNotEmpty()) {\n                        param.compressionType = param.compressionTypes[0]\n                    }\n                    param.compressionQuality = quality\n                    compressionApplied = true\n                } catch (e: Exception) {\n                    // Compression parameters cannot be applied, use default compression\n                }\n            }\n            \n            val imageOutput = ImageIO.createImageOutputStream(outputStream)\n            writer.output = imageOutput\n            writer.write(null, javax.imageio.IIOImage(image, null, null), param)\n            writer.dispose()\n            imageOutput?.close()\n        } else {\n            // Fallback: use default JPEG output\n            ImageIO.write(image, \"jpg\", outputStream)\n        }\n    }\n    \n    /**\n     * 压缩 PNG 图片\n     * PNG 使用 Deflate 压缩，compressionLevel 控制压缩级别\n     * compressionLevel: 0 = 最低压缩（快速），9 = 最高压缩（慢速）\n     * \n     * 注意：Java ImageIO 对 PNG 压缩的支持有限，某些实现可能不支持压缩参数\n     * 如果无法应用压缩参数，会使用默认的 PNG 输出（可能压缩效果较差）\n     * \n     * 优化：对于从 JPG 等有损格式转换来的图片，优化 BufferedImage 类型以提高压缩效率\n     */\n    private fun compressPNG(image: BufferedImage, compressionLevel: Int, outputStream: ByteArrayOutputStream) {\n        // 优化图片类型以提高压缩效率\n        // 如果图片不是标准的 RGB/ARGB 类型，转换为标准类型可以减少文件大小\n        val optimizedImage = if (image.type != BufferedImage.TYPE_INT_RGB && \n                                 image.type != BufferedImage.TYPE_INT_ARGB &&\n                                 image.type != BufferedImage.TYPE_3BYTE_BGR) {\n            // 检查是否有透明通道\n            val hasAlpha = image.colorModel.hasAlpha()\n            val targetType = if (hasAlpha) BufferedImage.TYPE_INT_ARGB else BufferedImage.TYPE_INT_RGB\n            \n            // 创建优化后的图片\n            val converted = BufferedImage(image.width, image.height, targetType)\n            val g: Graphics2D = converted.createGraphics() as Graphics2D\n            // 使用高质量渲染\n            g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR)\n            g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY)\n            g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)\n            g.drawImage(image, 0, 0, null)\n            g.dispose()\n            converted\n        } else {\n            image\n        }\n        \n        val writer: ImageWriter? = ImageIO.getImageWritersByFormatName(\"png\").next()\n        if (writer != null) {\n            val param = writer.defaultWriteParam\n            \n            // 尝试设置压缩参数\n            var compressionApplied = false\n            if (param.canWriteCompressed()) {\n                try {\n                    param.compressionMode = ImageWriteParam.MODE_EXPLICIT\n                    if (param.compressionTypes.isNotEmpty()) {\n                        param.compressionType = param.compressionTypes[0]\n                    }\n                    // PNG 压缩级别 0-9，9 是最高压缩率\n                    // ImageIO 的 compressionQuality 值越高压缩率越高，所以直接使用 compressionLevel / 9f\n                    param.compressionQuality = if (compressionLevel == 0) 0f else compressionLevel / 9f\n                    compressionApplied = true\n                } catch (e: Exception) {\n                    // Compression parameters cannot be applied, use default compression\n                }\n            }\n            \n            val imageOutput = ImageIO.createImageOutputStream(outputStream)\n            writer.output = imageOutput\n            writer.write(null, javax.imageio.IIOImage(optimizedImage, null, null), param)\n            writer.dispose()\n            imageOutput?.close()\n        } else {\n            // Fallback: use default PNG output\n            ImageIO.write(optimizedImage, \"png\", outputStream)\n        }\n    }\n    \n    /**\n     * 压缩 WebP 图片\n     * 注意：Java 内置不支持 WebP，需要外部库支持\n     * 这里采用降级处理，转换为 JPEG 或 PNG\n     * \n     * @return 是否使用了降级处理（true = 降级，false = 原生 WebP）\n     */\n    private fun compressWebP(\n        image: BufferedImage,\n        quality: Float,\n        isLossy: Boolean,\n        outputStream: ByteArrayOutputStream\n    ): Boolean {\n        // 检查是否支持 WebP\n        if (!isWebPSupported()) {\n            // WebP requires third-party library support (e.g., webp-imageio), fallback for now\n            if (isLossy) {\n                // WebP Lossy fallback to JPEG\n                compressJPEG(image, quality, outputStream)\n            } else {\n                // WebP Lossless fallback to PNG\n                compressPNG(image, 9, outputStream)\n            }\n            return true // Return true to indicate fallback was used\n        }\n        \n        // If WebP is supported, try to use native WebP encoding\n        // Note: WebP encoding library support is required here\n        // Still using fallback for now\n        if (isLossy) {\n            compressJPEG(image, quality, outputStream)\n        } else {\n            compressPNG(image, 9, outputStream)\n        }\n        return true\n    }\n    \n    /**\n     * 压缩并保存图片到文件\n     * \n     * @param image BufferedImage 图像对象\n     * @param outputFile 输出文件路径\n     * @param params 压缩参数\n     * @return 压缩后的文件大小（字节），失败返回 -1\n     */\n    /**\n     * 压缩并保存图片到文件\n     * \n     * @param image BufferedImage 图像对象\n     * @param outputFile 输出文件路径\n     * @param params 压缩参数\n     * @param originalFile 原始文件（可选，用于格式检测）\n     * @return Pair<压缩后的文件大小（字节）, 是否使用了降级处理>，失败返回 null\n     */\n    data class SaveResult(\n        val outputFile: File,\n        val sizeBytes: Long,\n        val usedFallback: Boolean\n    )\n\n    private fun resolveActualExtension(params: CompressionParams, usedFallback: Boolean): String {\n        return when (params.algorithm) {\n            CompressionAlgorithm.WEBP_LOSSY ->\n                if (usedFallback) \"jpg\" else \"webp\"\n            CompressionAlgorithm.WEBP_LOSSLESS ->\n                if (usedFallback) \"png\" else \"webp\"\n            else -> params.algorithm.format\n        }\n    }\n\n    fun saveCompressedData(\n        outputFile: File,\n        params: CompressionParams,\n        compressedData: ByteArray,\n        usedFallback: Boolean\n    ): SaveResult {\n        val ext = resolveActualExtension(params, usedFallback)\n        val baseName = outputFile.nameWithoutExtension.ifBlank { \"compressed\" }\n        val finalFile = File(outputFile.parentFile ?: File(\".\"), \"$baseName.$ext\")\n        finalFile.writeBytes(compressedData)\n        return SaveResult(\n            outputFile = finalFile,\n            sizeBytes = compressedData.size.toLong(),\n            usedFallback = usedFallback\n        )\n    }\n\n    fun compressAndSaveImage(\n        image: BufferedImage,\n        outputFile: File,\n        params: CompressionParams,\n        originalFile: File? = null\n    ): SaveResult? {\n        return try {\n            val result = compressImage(image, params) ?: return null\n            val (compressedData, usedFallback) = result\n\n            saveCompressedData(\n                outputFile = outputFile,\n                params = params,\n                compressedData = compressedData,\n                usedFallback = usedFallback\n            )\n        } catch (e: Exception) {\n            logger.error(\"Failed to save compressed image\", e)\n            null\n        }\n    }\n    \n    /**\n     * 检测格式转换是否可能导致文件变大\n     * \n     * @param originalFile 原始文件\n     * @param targetAlgorithm 目标压缩算法\n     * @return 如果转换可能导致文件变大，返回警告信息的 key，否则返回 null\n     */\n    fun checkFormatConversionWarning(\n        originalFile: File?,\n        targetAlgorithm: CompressionAlgorithm\n    ): String? {\n        if (originalFile == null) return null\n        \n        val originalFormat = ImageFormatDetector.detectFormat(originalFile)\n        \n        return when {\n            // JPG 转 PNG：PNG 是无损格式，通常比 JPG 大\n            originalFormat == ImageFormat.JPEG && targetAlgorithm == CompressionAlgorithm.PNG_OPTIMIZATION -> {\n                \"format_conversion_warning_jpg_to_png\"\n            }\n            // JPG 转 WebP Lossless：同样可能变大\n            originalFormat == ImageFormat.JPEG && targetAlgorithm == CompressionAlgorithm.WEBP_LOSSLESS -> {\n                \"format_conversion_warning_jpg_to_webp_lossless\"\n            }\n            else -> null\n        }\n    }\n    \n    /**\n     * 压缩并保存图片到文件（兼容旧接口）\n     */\n    @Deprecated(\"使用 compressAndSaveImage 替代，可以获取降级信息\")\n    fun compressAndSaveImageLegacy(\n        image: BufferedImage,\n        outputFile: File,\n        params: CompressionParams\n    ): Long {\n        return compressAndSaveImage(image, outputFile, params)?.sizeBytes ?: -1\n    }\n    \n    /**\n     * 批量压缩文件夹中的所有图片\n     * \n     * @param sourceDir 源文件夹\n     * @param outputDir 输出文件夹\n     * @param params 压缩参数\n     * @return 成功压缩的文件数\n     */\n    fun compressBatch(\n        sourceDir: File,\n        outputDir: File,\n        params: CompressionParams\n    ): Int {\n        return try {\n            if (!sourceDir.isDirectory) {\n                logger.error(\"Source path is not a directory: ${sourceDir.absolutePath}\")\n                return 0\n            }\n            \n            if (!outputDir.exists()) {\n                outputDir.mkdirs()\n            }\n            \n            val imageExtensions = setOf(\"jpg\", \"jpeg\", \"png\", \"bmp\", \"gif\", \"tiff\")\n            val imageFiles = sourceDir.listFiles { file ->\n                file.isFile && file.extension.lowercase() in imageExtensions\n            } ?: emptyArray()\n            \n            var successCount = 0\n            \n            imageFiles.forEach { file ->\n                try {\n                    val image = ImageIO.read(file) ?: return@forEach\n                    \n                    // 生成输出文件名\n                    val baseName = file.nameWithoutExtension\n                    val outputFileName = \"$baseName.${params.algorithm.format}\"\n                    val outputFile = File(outputDir, outputFileName)\n                    \n                    // 压缩并保存\n                    val result = compressAndSaveImage(image, outputFile, params)\n                    \n                    if (result != null) {\n                        successCount++\n                    }\n                } catch (e: Exception) {\n                    logger.error(\"Failed to process image: ${file.absolutePath}\", e)\n                }\n            }\n            \n            successCount\n        } catch (e: Exception) {\n            logger.error(\"Batch compression error\", e)\n            0\n        }\n    }\n    \n    /**\n     * 计算压缩效果\n     * \n     * @param originalSize 原始大小（字节）\n     * @param compressedSize 压缩后大小（字节）\n     * @return 压缩率百分比\n     */\n    fun calculateCompressionRatio(originalSize: Long, compressedSize: Long): Int {\n        return if (originalSize > 0) {\n            (100 * (1 - compressedSize.toDouble() / originalSize)).toInt()\n        } else {\n            0\n        }\n    }\n    \n    /**\n     * 格式化文件大小为可读的字符串\n     */\n    fun formatFileSize(bytes: Long): String {\n        return when {\n            bytes <= 0 -> \"0 B\"\n            bytes < 1024 -> \"$bytes B\"\n            bytes < 1024 * 1024 -> \"${bytes / 1024} KB\"\n            bytes < 1024 * 1024 * 1024 -> \"${String.format(\"%.2f\", bytes / (1024.0 * 1024.0))} MB\"\n            else -> \"${String.format(\"%.2f\", bytes / (1024.0 * 1024.0 * 1024.0))} GB\"\n        }\n    }\n}\n\n"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/utils/ImageFormatDetector.kt",
    "content": "package cn.netdiscovery.monica.utils\n\nimport java.io.File\nimport java.io.FileInputStream\nimport java.nio.charset.StandardCharsets\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.utils.ImageFormatDetector\n * @author: Tony Shen\n * @date: 2025/6/4 16:29\n * @version: V1.0 基于文件的文件头，来判断文件的格式\n */\nenum class ImageFormat {\n    JPEG, PNG, WEBP, HEIC, AVIF, BMP, TIFF, GIF,\n    PSD, HDR, SVG,\n    CR2, CR3, ARW, NEF, ORF, RAF, RW2, DNG,\n    UNKNOWN\n}\n\nfun ImageFormat.isRaw(): Boolean = this in listOf(\n    ImageFormat.CR2, ImageFormat.CR3, ImageFormat.ARW,\n    ImageFormat.NEF, ImageFormat.RAF, ImageFormat.ORF,\n    ImageFormat.RW2, ImageFormat.DNG\n)\n\nobject ImageFormatDetector {\n\n    fun detectFormat(file: File): ImageFormat {\n        val header = readFileHeader(file, 32)\n\n        return when {\n            // JPEG\n            header.startsWith(0xFF, 0xD8, 0xFF) -> ImageFormat.JPEG\n\n            // PNG\n            header.startsWith(\"89504E470D0A1A0A\") -> ImageFormat.PNG\n\n            // WEBP (RIFFxxxxWEBP)\n            header.startsWith(\"52494646\") && header.slice(8, 12).toAscii() == \"WEBP\" -> ImageFormat.WEBP\n\n            // HEIC\n            header.slice(4, 12).toAscii().startsWith(\"ftypheic\") -> ImageFormat.HEIC\n\n            // AVIF\n            header.slice(4, 12).toAscii().startsWith(\"ftypavif\") -> ImageFormat.AVIF\n\n            // BMP\n            header.startsWith(\"424D\") -> ImageFormat.BMP\n\n            // GIF\n            header.startsWith(\"47494638\") -> ImageFormat.GIF\n\n            // PSD\n            header.startsWith(\"38425053\") -> ImageFormat.PSD\n\n            // HDR (ASCII header)\n            header.startsWith(\"#?R\".toByteArray(StandardCharsets.US_ASCII)) -> ImageFormat.HDR\n\n            // SVG (text-based, starts with <svg or <?xml)\n            header.toAscii().trimStart().startsWith(\"<svg\") || header.toAscii().trimStart().startsWith(\"<?xml\") -> ImageFormat.SVG\n\n            // Canon CR2\n            header.startsWith(\"49492A00\") && header.size >= 12 && header.slice(8, 10).contentEquals(\"CR\".toByteArray()) -> ImageFormat.CR2\n\n            // Canon CR3 (ISO BMFF format with ftypcrx)\n            header.slice(4, 12).toAscii().startsWith(\"ftypcrx\") -> ImageFormat.CR3\n\n            // Sony ARW\n            header.startsWith(\"49492A00\") && header.slice(8, 12).contentEquals(\"ARW \".toByteArray()) -> ImageFormat.ARW\n\n            // Nikon NEF\n            header.startsWith(\"4D4D002A\") && header.slice(8, 12).contentEquals(\"NEF\".toByteArray()) -> ImageFormat.NEF\n\n            // Olympus ORF\n            header.slice(0, 4).contentEquals(\"IIRO\".toByteArray()) -> ImageFormat.ORF\n\n            // Panasonic RW2\n            header.startsWith(\"49492A00\") && header.slice(8, 12).contentEquals(\"RW2\".toByteArray()) -> ImageFormat.RW2\n\n            // Fuji RAF\n            header.startsWith(\"4655494A\") -> ImageFormat.RAF\n\n            // DNG\n            header.startsWith(\"49492A00\") && header.containsSubsequence(\"Adobe\".toByteArray()) -> ImageFormat.DNG\n\n            // TIFF fallback\n            header.startsWith(\"49492A00\") || header.startsWith(\"4D4D002A\") -> ImageFormat.TIFF\n\n            else -> ImageFormat.UNKNOWN\n        }\n    }\n\n    fun getImageFormat(file: File): String? {\n        val imageFormat = detectFormat(file)\n\n        return if (imageFormat!=ImageFormat.UNKNOWN) {\n            imageFormat.name.lowercase()\n        } else {\n            null\n        }\n    }\n\n    // 读取文件前 N 字节作为头部\n    private fun readFileHeader(file: File, size: Int): ByteArray {\n        FileInputStream(file).use { input ->\n            return input.readNBytes(size)\n        }\n    }\n\n    // 二进制 startsWith 判断\n    private fun ByteArray.startsWith(vararg bytes: Int): Boolean {\n        if (this.size < bytes.size) return false\n        for (i in bytes.indices) {\n            if (this[i].toInt() and 0xFF != bytes[i]) return false\n        }\n        return true\n    }\n\n    // Hex 字符串形式 startsWith 判断\n    private fun ByteArray.startsWith(hex: String): Boolean {\n        val bytes = hex.chunked(2).map { it.toInt(16) }\n        return startsWith(*bytes.toIntArray())\n    }\n\n    // ByteArray 对比\n    private fun ByteArray.startsWith(prefix: ByteArray): Boolean {\n        if (this.size < prefix.size) return false\n        for (i in prefix.indices) {\n            if (this[i] != prefix[i]) return false\n        }\n        return true\n    }\n\n    // ByteArray 区间切片\n    private fun ByteArray.slice(from: Int, to: Int): ByteArray {\n        return copyOfRange(from, to.coerceAtMost(this.size))\n    }\n\n    // 判断是否包含某一子序列\n    private fun ByteArray.containsSubsequence(seq: ByteArray): Boolean {\n        outer@ for (i in 0..(this.size - seq.size)) {\n            for (j in seq.indices) {\n                if (this[i + j] != seq[j]) continue@outer\n            }\n            return true\n        }\n        return false\n    }\n\n    // 将 ByteArray 转成 ASCII 字符串（安全用于文件头分析）\n    private fun ByteArray.toAscii(): String = String(this, Charsets.US_ASCII)\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/utils/ImageUtils.kt",
    "content": "package cn.netdiscovery.monica.utils\n\nimport androidx.compose.ui.graphics.toAwtImage\nimport androidx.compose.ui.graphics.toComposeImageBitmap\nimport cn.netdiscovery.monica.exception.MonicaException\nimport cn.netdiscovery.monica.imageprocess.BufferedImages\nimport cn.netdiscovery.monica.imageprocess.filter.*\nimport cn.netdiscovery.monica.imageprocess.filter.blur.*\nimport cn.netdiscovery.monica.imageprocess.filter.sharpen.LaplaceSharpenFilter\nimport cn.netdiscovery.monica.imageprocess.filter.sharpen.SharpenFilter\nimport cn.netdiscovery.monica.imageprocess.filter.sharpen.USMFilter\nimport cn.netdiscovery.monica.imageprocess.utils.extension.convertToRGB\nimport cn.netdiscovery.monica.imageprocess.utils.loadFixedSvgAsImage\nimport cn.netdiscovery.monica.opencv.ImageProcess\nimport cn.netdiscovery.monica.state.ApplicationState\nimport cn.netdiscovery.monica.utils.extensions.printConstructorParamsWithValues\nimport com.safframework.kotlin.coroutines.IO\nimport kotlinx.coroutines.withContext\nimport org.slf4j.Logger\nimport org.slf4j.LoggerFactory\nimport java.awt.image.BufferedImage\nimport java.io.File\nimport javax.imageio.ImageIO\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.imageprocess.ImageUtils\n * @author: Tony Shen\n * @date: 2024/4/26 22:11\n * @version: V1.0 <描述当前版本功能>\n */\nprivate val logger: Logger = LoggerFactory.getLogger(object : Any() {}.javaClass.enclosingClass)\n\nfun getBufferedImage(file: File, state: ApplicationState): BufferedImage {\n\n    val filePath = file.absolutePath\n\n    val imageFormat = ImageFormatDetector.detectFormat(file)\n    logger.info(\"format: $imageFormat\")\n\n    if (imageFormat.isRaw()) {\n        try {\n            val decodedPreviewImage = ImageProcess.decodeRawToBufferForPreView(filePath)\n            if (decodedPreviewImage!=null) {\n                state.nativeImageInfo = decodedPreviewImage\n                state.rawImageFormat = imageFormat\n\n                val outPixels = decodedPreviewImage.previewImage\n                val width = decodedPreviewImage.width\n                val height = decodedPreviewImage.height\n                val image = BufferedImages.toBufferedImage(outPixels, width, height, BufferedImage.TYPE_INT_ARGB)\n                return image\n            } else {\n                throw MonicaException(\"Image format is not supported\")\n            }\n        } catch (e:Exception) {\n            logger.error(\"decode raw image failed\", e)\n            throw MonicaException(\"decode raw image failed\")\n        }\n    } else {\n        return when(imageFormat) {\n            ImageFormat.SVG -> loadFixedSvgAsImage(file) ?: ImageIO.read(file)\n            ImageFormat.HDR -> {\n                ImageIO.read(file).convertToRGB()\n            }\n            ImageFormat.JPEG, ImageFormat.PNG, ImageFormat.WEBP -> {\n                ImageIO.read(file)\n            }\n            ImageFormat.HEIC -> {\n                try {\n                    val decodedPreviewImage = ImageProcess.decodeHeif(filePath)\n                    if (decodedPreviewImage!=null) {\n                        state.nativeImageInfo = decodedPreviewImage\n                        state.rawImageFormat = imageFormat\n\n                        val outPixels = decodedPreviewImage.previewImage\n                        val width = decodedPreviewImage.width\n                        val height = decodedPreviewImage.height\n                        val image = BufferedImages.toBufferedImage(outPixels, width, height, BufferedImage.TYPE_INT_ARGB)\n                        return image\n                    }  else {\n                        throw MonicaException(\"Image format is not supported\")\n                    }\n                } catch (e: Exception) {\n                    logger.error(\"decode heif image failed\", e)\n                    throw MonicaException(\"decode heif image failed\")\n                }\n            }\n            else -> throw MonicaException(\"Unsupported image format: $imageFormat\")\n        }\n    }\n}\n\nfun getBufferedImage(file: File): BufferedImage {\n\n    val filePath = file.absolutePath\n\n    val imageFormat = ImageFormatDetector.detectFormat(file)\n    logger.info(\"format: $imageFormat\")\n\n    if (imageFormat.isRaw()) {\n        val decodedPreviewImage = ImageProcess.decodeRawToBufferForPreView(filePath)\n        if (decodedPreviewImage!=null) {\n            val outPixels = decodedPreviewImage.previewImage\n            val width = decodedPreviewImage.width\n            val height = decodedPreviewImage.height\n            val image = BufferedImages.toBufferedImage(outPixels,width,height,BufferedImage.TYPE_INT_ARGB)\n            return image\n        } else {\n            throw MonicaException(\"Image format is not supported\")\n        }\n    } else {\n        return when(imageFormat) {\n            ImageFormat.SVG -> loadFixedSvgAsImage(file) ?: ImageIO.read(file)\n            ImageFormat.HDR -> {\n                ImageIO.read(file).convertToRGB()\n            }\n            ImageFormat.JPEG, ImageFormat.PNG, ImageFormat.WEBP -> {\n                ImageIO.read(file)\n            }\n            ImageFormat.HEIC -> {\n                val decodedPreviewImage = ImageProcess.decodeHeif(filePath)\n                if (decodedPreviewImage!=null) {\n                    val outPixels = decodedPreviewImage.previewImage\n                    val width = decodedPreviewImage.width\n                    val height = decodedPreviewImage.height\n                    val image = BufferedImages.toBufferedImage(outPixels, width, height, BufferedImage.TYPE_INT_ARGB)\n                    return image\n                }  else {\n                    throw MonicaException(\"Image format is not supported\")\n                }\n            }\n            else -> throw MonicaException(\"Unsupported image format: $imageFormat\")\n        }\n    }\n}\n\nsuspend fun doFilter(\n    filterName: String,\n    array: MutableList<Any>,\n    image: BufferedImage\n): BufferedImage {\n\n    return withContext(IO) {\n        when(filterName) {\n            \"AverageFilter\" -> {\n                AverageFilter().transform(image)\n            }\n            \"BilateralFilter\" -> {\n                val filter = BilateralFilter(array[0] as Double, array[1] as Double)\n                filter.printConstructorParamsWithValues()\n                filter.transform(image)\n            }\n            \"BlockFilter\" -> {\n                val filter = BlockFilter(array[0] as Int)\n                filter.printConstructorParamsWithValues()\n                filter.transform(image)\n            }\n            \"BoxBlurFilter\" -> {\n                val filter = BoxBlurFilter(array[0] as Int,array[2] as Int,array[1] as Int)\n                filter.printConstructorParamsWithValues()\n                filter.transform(image)\n            }\n            \"BumpFilter\" -> {\n                BumpFilter().transform(image)\n            }\n            \"CarveFilter\" -> {\n                val filter = CarveFilter()\n                filter.transform(image)\n            }\n            \"ColorFilter\" -> {\n                val filter = ColorFilter(array[0] as Int)\n                filter.printConstructorParamsWithValues()\n                filter.transform(image)\n            }\n            \"ColorHalftoneFilter\" -> {\n                val filter = ColorHalftoneFilter(array[0] as Float)\n                filter.printConstructorParamsWithValues()\n                filter.transform(image)\n            }\n            \"ConBriFilter\" -> {\n                val filter = ConBriFilter(array[1] as Float,array[0] as Float)\n                filter.printConstructorParamsWithValues()\n                filter.transform(image)\n            }\n            \"CropFilter\" -> {\n                val filter = CropFilter(array[2] as Int,array[3] as Int,array[1] as Int,array[0] as Int)\n                filter.printConstructorParamsWithValues()\n                filter.transform(image)\n            }\n            \"CrystallizeFilter\" -> {\n                val filter = CrystallizeFilter(array[0] as Float, array[3] as Float, array[2] as Float, array[1] as Int)\n                filter.printConstructorParamsWithValues()\n                filter.transform(image)\n            }\n            \"DiffuseFilter\" -> {\n                val filter = DiffuseFilter(array[0] as Float)\n                filter.printConstructorParamsWithValues()\n                filter.transform(image)\n            }\n            \"EmbossFilter\" -> {\n                val filter = EmbossFilter(array[0] as Int)\n                filter.printConstructorParamsWithValues()\n                filter.transform(image)\n            }\n            \"EqualizeFilter\" -> {\n                val filter = EqualizeFilter()\n                filter.transform(image)\n            }\n            \"ExposureFilter\" -> {\n                val filter = ExposureFilter(array[0] as Float)\n                filter.printConstructorParamsWithValues()\n                filter.transform(image)\n            }\n            \"GainFilter\" -> {\n                val filter = GainFilter(array[1] as Float, array[0] as Float)\n                filter.printConstructorParamsWithValues()\n                filter.transform(image)\n            }\n            \"GammaFilter\" -> {\n                val filter = GammaFilter(array[0] as Double)\n                filter.printConstructorParamsWithValues()\n                filter.transform(image)\n            }\n            \"FastBlur2D\" -> {\n                FastBlur2D(array[0] as Int).transform(image)\n            }\n            \"GaussianFilter\" -> {\n                val filter = GaussianFilter(array[0] as Float)\n                filter.printConstructorParamsWithValues()\n                filter.transform(image)\n            }\n            \"GaussianNoiseFilter\" -> {\n                val filter = GaussianNoiseFilter(array[0] as Int)\n                filter.printConstructorParamsWithValues()\n                filter.transform(image)\n            }\n            \"GradientFilter\" -> {\n                GradientFilter().transform(image)\n            }\n            \"GrayFilter\" -> {\n                GrayFilter().transform(image)\n            }\n            \"HighPassFilter\" -> {\n                val filter = HighPassFilter(array[0] as Float)\n                filter.printConstructorParamsWithValues()\n                filter.transform(image)\n            }\n            \"HSBAdjustFilter\" -> {\n                val filter = HSBAdjustFilter(array[1] as Float, array[2] as Float, array[0] as Float)\n                filter.printConstructorParamsWithValues()\n                filter.transform(image)\n            }\n            \"InvertFilter\" -> {\n                InvertFilter().transform(image)\n            }\n            \"LaplaceSharpenFilter\" -> {\n                LaplaceSharpenFilter().transform(image)\n            }\n            \"LensBlurFilter\" -> {\n                val filter = LensBlurFilter(array[3] as Float,array[1] as Float,array[2] as Float,array[0] as Float,array[4] as Int)\n                filter.printConstructorParamsWithValues()\n                filter.transform(image)\n            }\n            \"MarbleFilter\" -> {\n                val filter = MarbleFilter(array[1] as Float,array[2] as Float,array[0] as Float)\n                filter.printConstructorParamsWithValues()\n                filter.transform(image)\n            }\n            \"MaximumFilter\" -> {\n                val filter = MaximumFilter()\n                filter.printConstructorParamsWithValues()\n                filter.transform(image)\n            }\n            \"MinimumFilter\" -> {\n                val filter = MinimumFilter()\n                filter.printConstructorParamsWithValues()\n                filter.transform(image)\n            }\n            \"MirrorFilter\" -> {\n                val filter = MirrorFilter(array[2] as Float,array[0] as Float,array[1] as Float)\n                filter.printConstructorParamsWithValues()\n                filter.transform(image)\n            }\n            \"MosaicFilter\" -> {\n                val filter = MosaicFilter(array[0] as Int)\n                filter.printConstructorParamsWithValues()\n                filter.transform(image)\n            }\n            \"MotionFilter\" -> {\n                val filter = MotionFilter(array[1] as Float,array[0] as Float,array[2] as Float)\n                filter.printConstructorParamsWithValues()\n                filter.transform(image)\n            }\n            \"NatureFilter\" -> {\n                val filter = NatureFilter(array[0] as Int)\n                filter.printConstructorParamsWithValues()\n                filter.transform(image)\n            }\n            \"OffsetFilter\" -> {\n                val filter = OffsetFilter(array[0] as Int,array[1] as Int)\n                filter.printConstructorParamsWithValues()\n                filter.transform(image)\n            }\n            \"OilPaintFilter\" -> {\n                val filter = OilPaintFilter(array[1] as Int,array[0] as Int)\n                filter.printConstructorParamsWithValues()\n                filter.transform(image)\n            }\n            \"PointillizeFilter\" -> {\n                val filter = PointillizeFilter(array[0] as Float, array[1] as Float, array[4] as Float, array[3] as Float, array[2] as Int)\n                filter.printConstructorParamsWithValues()\n                filter.transform(image)\n            }\n            \"PosterizeFilter\" -> {\n                val filter = PosterizeFilter(array[0] as Int)\n                filter.printConstructorParamsWithValues()\n                filter.transform(image)\n            }\n            \"RippleFilter\" -> {\n                val filter = RippleFilter(array[1] as Float, array[3] as Float, array[2] as Float, array[4] as Float, array[0] as Int)\n                filter.printConstructorParamsWithValues()\n                filter.transform(image)\n            }\n            \"SepiaToneFilter\" -> {\n                SepiaToneFilter().transform(image)\n            }\n            \"SharpenFilter\" -> {\n                SharpenFilter().transform(image)\n            }\n            \"SmearFilter\" -> {\n                val filter = SmearFilter(array[0] as Float, array[1] as Float, array[2] as Int, array[4] as Int, array[3] as Float)\n                filter.printConstructorParamsWithValues()\n                filter.transform(image)\n            }\n            \"SolarizeFilter\" -> {\n                SolarizeFilter().transform(image)\n            }\n            \"SpotlightFilter\" -> {\n                val filter = SpotlightFilter(array[0] as Int)\n                filter.printConstructorParamsWithValues()\n                filter.transform(image)\n            }\n            \"StrokeAreaFilter\" -> {\n                val filter = StrokeAreaFilter(array[0] as Double)\n                filter.printConstructorParamsWithValues()\n                filter.transform(image)\n            }\n            \"SwimFilter\" -> {\n                val filter = SwimFilter(array[2] as Float,array[3] as Float,array[1] as Float,array[0] as Float,array[5] as Float,array[4] as Float,)\n                filter.printConstructorParamsWithValues()\n                filter.transform(image)\n            }\n            \"USMFilter\" -> {\n                val filter = USMFilter(array[1] as Float,array[0] as Float,array[2] as Int)\n                filter.printConstructorParamsWithValues()\n                filter.transform(image.toComposeImageBitmap().toAwtImage())\n            }\n            \"VariableBlurFilter\"-> {\n                val filter = VariableBlurFilter(array[0] as Int,array[2] as Int,array[1] as Int)\n                filter.printConstructorParamsWithValues()\n                filter.transform(image)\n            }\n            \"VignetteFilter\"-> {\n                val filter = VignetteFilter(array[0] as Int,array[1] as Int)\n                filter.printConstructorParamsWithValues()\n                filter.transform(image)\n            }\n            \"WaterFilter\" -> {\n                val filter = WaterFilter(array[5] as Float, array[0] as Float, array[3] as Float, array[1] as Float, array[2] as Float, array[4] as Float)\n                filter.printConstructorParamsWithValues()\n                filter.transform(image)\n            }\n            \"WhiteImageFilter\" -> {\n                val filter = WhiteImageFilter(array[0] as Double)\n                filter.printConstructorParamsWithValues()\n                filter.transform(image)\n            }\n\n            else -> {\n                image\n            }\n        }\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/utils/LogHomeProperty.kt",
    "content": "package cn.netdiscovery.monica.utils\n\nimport ch.qos.logback.core.PropertyDefinerBase\nimport java.io.File\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.utils.LogHomeProperty\n * @author: Tony Shen\n * @date: 2022/4/21 4:23 下午\n * @version: V1.0 <描述当前版本功能>\n */\nclass LogHomeProperty : PropertyDefinerBase() {\n\n    override fun getPropertyValue(): String {\n\n        return AppDirs.logDir.absolutePath + File.separator\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/utils/LogUtils.kt",
    "content": "package cn.netdiscovery.monica.utils\n\nimport org.slf4j.Logger\nimport org.slf4j.LoggerFactory\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.utils.LogUtils\n * @author: Tony Shen\n * @date: 2024/7/10 14:03\n * @version: V1.0 <描述当前版本功能>\n */\ninline fun <reified T> logger(): Logger = LoggerFactory.getLogger(T::class.java)"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/utils/ScreenshotUtils.kt",
    "content": "package cn.netdiscovery.monica.utils\n\nimport cn.netdiscovery.monica.state.ApplicationState\nimport cn.netdiscovery.monica.utils.extensions.launchWithLoading\nimport org.slf4j.Logger\nimport org.slf4j.LoggerFactory\nimport java.awt.Rectangle\nimport java.awt.Robot\nimport java.awt.Toolkit\nimport java.awt.image.BufferedImage\n\n/**\n * 截图工具类\n * \n * @author: Tony Shen\n * @date: 2025/12/03\n * @version: V1.0\n */\nprivate val logger: Logger = LoggerFactory.getLogger(object : Any() {}.javaClass.enclosingClass)\n\n/**\n * 截取整个屏幕\n */\nfun captureFullScreen(): BufferedImage? {\n    return try {\n        val robot = Robot()\n        val screenSize = Toolkit.getDefaultToolkit().screenSize\n        robot.createScreenCapture(Rectangle(0, 0, screenSize.width, screenSize.height))\n    } catch (e: Exception) {\n        logger.error(\"截取全屏失败\", e)\n        null\n    }\n}\n\n/**\n * 截取指定区域\n * @param x 起始 X 坐标\n * @param y 起始 Y 坐标\n * @param width 宽度\n * @param height 高度\n */\nfun captureRegion(x: Int, y: Int, width: Int, height: Int): BufferedImage? {\n    return try {\n        val robot = Robot()\n        robot.createScreenCapture(Rectangle(x, y, width, height))\n    } catch (e: Exception) {\n        logger.error(\"截取区域失败: x=$x, y=$y, width=$width, height=$height\", e)\n        null\n    }\n}\n\n/**\n * 将截图加载到 ApplicationState\n */\nfun loadScreenshotToState(state: ApplicationState, screenshot: BufferedImage) {\n    state.scope.launchWithLoading {\n        try {\n            logger.info(\"加载截图到应用状态\")\n            state.rawImage = screenshot\n            state.currentImage = state.rawImage\n            state.rawImageFile = null\n        } catch (e: Exception) {\n            logger.error(\"加载截图失败\", e)\n        }\n    }\n}\n\n"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/utils/TextUtils.kt",
    "content": "package cn.netdiscovery.monica.utils\n\nimport java.text.Collator\nimport java.util.*\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.utils.TextUtils\n * @author: Tony Shen\n * @date: 2024/5/11 14:05\n * @version: V1.0 <描述当前版本功能>\n */\nval collator:Collator by lazy {\n    Collator.getInstance(Locale.UK)\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/utils/TimeUtils.kt",
    "content": "package cn.netdiscovery.monica.utils\n\nimport java.text.SimpleDateFormat\nimport java.time.ZoneId\nimport java.time.ZonedDateTime\nimport java.time.format.DateTimeFormatter\nimport java.util.Locale\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.utils.TimeUtils\n * @author: Tony Shen\n * @date: 2024/5/2 21:40\n * @version: V1.0 <描述当前版本功能>\n */\n\nprivate const val yyyy_MM_dd_HH_mm_ss_SSS = \"yyyy-MM-dd-H-mm-ss-SSS\"\n\nprivate val formatterWithHorizontal by lazy {\n    DateTimeFormatter.ofPattern(yyyy_MM_dd_HH_mm_ss_SSS).withZone(ZoneId.systemDefault())\n}\n\nval formatTimestamp by lazy {\n    SimpleDateFormat(\"yyyy-MM-dd HH:mm:ss\", Locale.getDefault())\n}\n\n/**\n * 生成图片的名称\n */\nfun currentTime(): String = ZonedDateTime.now().format(formatterWithHorizontal)\n"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/utils/Typealiases.kt",
    "content": "package cn.netdiscovery.monica.utils\n\nimport cn.netdiscovery.monica.ui.controlpanel.cropimage.setting.CropProperties\nimport java.awt.image.BufferedImage\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.utils.Typealiases\n * @author: Tony Shen\n * @date:  2024/9/21 14:56\n * @version: V1.0 <描述当前版本功能>\n */\ntypealias CVAction = (byteArray:ByteArray) -> IntArray\n\ntypealias CVSuccess = (image: BufferedImage)->Unit\n\ntypealias CVFailure = (e:Exception) -> Unit\n\ntypealias OnCropPropertiesChange = (cropProperties: CropProperties) -> Unit\n\ntypealias Action = () -> Unit"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/utils/Validation.kt",
    "content": "package cn.netdiscovery.monica.utils\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.utils.Validation\n * @author: Tony Shen\n * @date: 2024/10/25 10:24\n * @version: V1.0 <描述当前版本功能>\n */\n\n/**\n * 对字段进行转换和验证\n * @param block 对字段进行转换\n * @param failed 对字段转换失败的回调\n */\nfun <T> getValidateField(block:()-> T,\n                         failed:()->Unit): T? {\n\n    return try {\n        block.invoke()\n    } catch (e:Exception) {\n        failed.invoke()\n        null\n    }\n}\n\n/**\n * 对字段进行转换和验证\n * @param block 对字段进行转换\n * @param condition 对字段的值进行校验\n * @param failed 对字段转换失败/校验失败的回调\n */\nfun <T> getValidateField(block:()-> T,\n                         condition: (T) -> Boolean,\n                         failed:()->Unit): T? {\n\n    return try {\n        val field = block.invoke()\n        if (condition.invoke(field)) {\n            field\n        } else {\n            failed.invoke()\n            null\n        }\n    } catch (e:Exception) {\n        failed.invoke()\n        null\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/utils/WebScreenshotUtils.kt",
    "content": "package cn.netdiscovery.monica.utils\n\nimport cn.netdiscovery.monica.config.arch\nimport cn.netdiscovery.monica.config.isLinux\nimport cn.netdiscovery.monica.config.isMac\nimport cn.netdiscovery.monica.config.isWindows\nimport cn.netdiscovery.monica.exception.ErrorSeverity\nimport cn.netdiscovery.monica.exception.ErrorType\nimport cn.netdiscovery.monica.exception.showError\nimport cn.netdiscovery.monica.state.ApplicationState\nimport cn.netdiscovery.monica.utils.extensions.launchWithSuspendLoading\nimport com.google.gson.Gson\nimport com.google.gson.JsonObject\nimport com.google.gson.JsonParser\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\nimport org.slf4j.Logger\nimport org.slf4j.LoggerFactory\nimport java.awt.image.BufferedImage\nimport java.io.File\nimport java.io.FileOutputStream\nimport java.io.FileNotFoundException\nimport java.io.IOException\nimport java.nio.file.Files\nimport java.nio.file.StandardCopyOption\nimport java.util.concurrent.TimeUnit\nimport java.util.zip.ZipInputStream\nimport javax.imageio.ImageIO\nimport kotlin.concurrent.thread\n\n/**\n * 网页截图工具类\n * \n * @author: Tony Shen\n * @date: 2026/01/12\n * @version: V1.0\n */\nprivate val logger: Logger = LoggerFactory.getLogger(object : Any() {}.javaClass.enclosingClass)\n\nprivate data class NodeRuntime(\n    val executable: String,\n    val bundled: Boolean\n)\n\nprivate data class BundledWebRuntime(\n    val runtimeRoot: File,\n    val scriptFile: File,\n    val nodeFile: File?,\n    val browsersDir: File?\n)\n\n/**\n * Cookie 数据类\n */\ndata class Cookie(\n    val name: String,\n    val value: String,\n    val domain: String? = null,\n    val path: String? = null,\n    val expires: Long? = null,\n    val httpOnly: Boolean = false,\n    val secure: Boolean = false,\n    val sameSite: String? = null // \"Strict\", \"Lax\", \"None\"\n)\n\n/**\n * 网页截图配置\n */\ndata class WebScreenshotOptions(\n    val fullPage: Boolean = true,\n    val waitUntil: String = \"networkidle\", // load, domcontentloaded, networkidle\n    val timeout: Long = 30000, // 30秒\n    val viewportWidth: Int? = null,\n    val viewportHeight: Int? = null,\n    val deviceScaleFactor: Double = 2.0,\n    val cookies: List<Cookie> = emptyList() // Cookie 列表\n)\n\n/**\n * 检查 Node.js 是否已安装\n */\nprivate fun checkNodeInstalled(runtime: NodeRuntime): Boolean {\n    return try {\n        val process = ProcessBuilder(runtime.executable, \"--version\")\n            .redirectErrorStream(true)\n            .start()\n        val finished = process.waitFor(3, TimeUnit.SECONDS)\n        val result = finished && process.exitValue() == 0\n        process.destroy()\n        result\n    } catch (e: Exception) {\n        logger.debug(\"Node.js 检查失败: ${runtime.executable}\", e)\n        false\n    }\n}\n\nfun checkNodeInstalled(): Boolean = checkNodeInstalled(resolveNodeRuntime())\n\n/**\n * 获取当前平台的资源目录名\n */\nprivate fun getCurrentPlatformResourceDirName(): String? {\n    val normalizedArch = arch.lowercase()\n    return when {\n        isMac && (normalizedArch == \"aarch64\" || normalizedArch == \"arm64\") -> \"macos-arm64\"\n        isMac -> \"macos-x64\"\n        isWindows -> \"windows\"\n        isLinux && (normalizedArch == \"aarch64\" || normalizedArch == \"arm64\") -> \"linux-arm64\"\n        isLinux -> \"linux-x64\"\n        else -> null\n    }\n}\n\n/**\n * 获取网页截图资源搜索目录\n */\nprivate fun getWebScreenshotResourceDirs(): List<File> {\n    val resourceDirs = buildList {\n        val composeResourcesDir = System.getProperty(\"compose.application.resources.dir\")\n            ?.takeIf { it.isNotBlank() }\n            ?.let { File(it) }\n\n        composeResourcesDir?.let {\n            add(it)\n            add(File(it, \"common\"))\n        }\n\n        val projectResourcesDir = File(\"resources\")\n        add(projectResourcesDir)\n        add(File(projectResourcesDir, \"common\"))\n    }\n\n    return resourceDirs\n        .map { it.absoluteFile }\n        .distinctBy { it.path }\n}\n\n/**\n * 在资源目录中查找文件\n */\nprivate fun findResourceFile(relativePath: String): File? {\n    getWebScreenshotResourceDirs().forEach { dir ->\n        val file = File(dir, relativePath)\n        if (file.exists()) {\n            return file\n        }\n    }\n    return null\n}\n\nprivate fun getUserWebRuntimeBaseDir(): File {\n    val userHome = File(System.getProperty(\"user.home\"))\n    return when {\n        isMac -> File(userHome, \"Library/Application Support/Monica/web-screenshot-runtime\")\n        isWindows -> {\n            val appData = System.getenv(\"APPDATA\")?.takeIf { it.isNotBlank() }\n            val baseDir = appData?.let(::File) ?: File(userHome, \"AppData/Roaming\")\n            File(baseDir, \"Monica/web-screenshot-runtime\")\n        }\n        else -> {\n            val xdgDataHome = System.getenv(\"XDG_DATA_HOME\")?.takeIf { it.isNotBlank() }\n            val baseDir = xdgDataHome?.let(::File) ?: File(userHome, \".local/share\")\n            File(baseDir, \"Monica/web-screenshot-runtime\")\n        }\n    }\n}\n\nprivate fun getUserWebRuntimeRoot(): File? {\n    val platformDir = getCurrentPlatformResourceDirName() ?: return null\n    return File(getUserWebRuntimeBaseDir(), platformDir)\n}\n\nprivate fun getOfflineRuntimePayloadZip(): File? {\n    val platformDir = getCurrentPlatformResourceDirName() ?: return null\n    return findResourceFile(\"web-screenshot-runtime/$platformDir/runtime.zip\")\n}\n\nprivate fun getBundledNodeExecutable(runtimeRoot: File): File? {\n    val candidates = if (isWindows) {\n        listOf(\n            File(runtimeRoot, \"node/node.exe\"),\n            File(runtimeRoot, \"node.exe\")\n        )\n    } else {\n        listOf(\n            File(runtimeRoot, \"node/bin/node\"),\n            File(runtimeRoot, \"node/node\"),\n            File(runtimeRoot, \"bin/node\")\n        )\n    }\n\n    candidates.forEach { nodeFile ->\n        if (nodeFile.exists()) {\n            if (!isWindows && !nodeFile.canExecute()) {\n                nodeFile.setExecutable(true)\n            }\n            return nodeFile\n        }\n    }\n\n    return null\n}\n\nprivate fun getBundledPlaywrightBrowsersPath(runtimeRoot: File): File? {\n    val candidates = listOf(\n        File(runtimeRoot, \"node_modules/playwright-core/.local-browsers\"),\n        File(runtimeRoot, \"ms-playwright\")\n    )\n\n    candidates.forEach { browsersDir ->\n        if (browsersDir.exists()) {\n            return browsersDir\n        }\n    }\n\n    return null\n}\n\nprivate fun isRuntimeReady(runtimeRoot: File): Boolean {\n    val script = File(runtimeRoot, \"web-screenshot.js\")\n    val packageJson = File(runtimeRoot, \"package.json\")\n    val playwrightModule = File(runtimeRoot, \"node_modules/playwright/package.json\")\n    return script.exists() && packageJson.exists() && playwrightModule.exists()\n}\n\nprivate fun computeRuntimePayloadStamp(payloadZip: File): String =\n    \"${payloadZip.length()}:${payloadZip.lastModified()}\"\n\nprivate fun restoreRuntimeExecutablePermissions(runtimeRoot: File) {\n    if (isWindows) return\n\n    runtimeRoot.walkTopDown()\n        .filter { it.isFile }\n        .forEach { file ->\n            val relativePath = file.relativeTo(runtimeRoot).invariantSeparatorsPath\n            val fileName = file.name\n            val shouldBeExecutable =\n                relativePath == \"node/bin/node\" ||\n                    relativePath.endsWith(\"/headless_shell\") ||\n                    relativePath.endsWith(\"/chrome-headless-shell\") ||\n                    relativePath.endsWith(\"/Chromium\") ||\n                    relativePath.endsWith(\"/chrome\") ||\n                    fileName.startsWith(\"ffmpeg\")\n\n            if (shouldBeExecutable && !file.canExecute()) {\n                file.setExecutable(true)\n            }\n        }\n}\n\nprivate fun extractOfflineRuntime(payloadZip: File, runtimeRoot: File) {\n    val tempRoot = File(runtimeRoot.parentFile, \"${runtimeRoot.name}.tmp-${System.currentTimeMillis()}\")\n    if (tempRoot.exists()) {\n        tempRoot.deleteRecursively()\n    }\n    tempRoot.mkdirs()\n\n    ZipInputStream(payloadZip.inputStream().buffered()).use { zipInput ->\n        while (true) {\n            val entry = zipInput.nextEntry ?: break\n            val outputFile = File(tempRoot, entry.name)\n            if (entry.isDirectory) {\n                outputFile.mkdirs()\n            } else {\n                outputFile.parentFile?.mkdirs()\n                FileOutputStream(outputFile).use { output ->\n                    zipInput.copyTo(output)\n                }\n            }\n            zipInput.closeEntry()\n        }\n    }\n\n    restoreRuntimeExecutablePermissions(tempRoot)\n    File(tempRoot, \".payload-stamp\").writeText(computeRuntimePayloadStamp(payloadZip))\n\n    runtimeRoot.parentFile?.mkdirs()\n    if (runtimeRoot.exists()) {\n        runtimeRoot.deleteRecursively()\n    }\n    Files.move(tempRoot.toPath(), runtimeRoot.toPath(), StandardCopyOption.REPLACE_EXISTING)\n}\n\nprivate fun getLegacyBundledRuntime(): BundledWebRuntime? {\n    val resourceRoot = getWebScreenshotResourceDirs().firstOrNull { dir ->\n        File(dir, \"web-screenshot.js\").exists()\n    } ?: return null\n    return BundledWebRuntime(\n        runtimeRoot = resourceRoot,\n        scriptFile = File(resourceRoot, \"web-screenshot.js\"),\n        nodeFile = getBundledNodeExecutable(resourceRoot),\n        browsersDir = getBundledPlaywrightBrowsersPath(resourceRoot)\n    )\n}\n\nprivate fun ensureBundledWebRuntime(): BundledWebRuntime? {\n    val payloadZip = getOfflineRuntimePayloadZip() ?: return getLegacyBundledRuntime()\n    val runtimeRoot = getUserWebRuntimeRoot() ?: return getLegacyBundledRuntime()\n    val expectedStamp = computeRuntimePayloadStamp(payloadZip)\n    val stampFile = File(runtimeRoot, \".payload-stamp\")\n    val needsExtract = !isRuntimeReady(runtimeRoot) ||\n        !stampFile.exists() ||\n        stampFile.readText() != expectedStamp\n\n    if (needsExtract) {\n        logger.info(\"解压离线网页截图运行时到: ${runtimeRoot.absolutePath}\")\n        extractOfflineRuntime(payloadZip, runtimeRoot)\n    }\n\n    restoreRuntimeExecutablePermissions(runtimeRoot)\n\n    return BundledWebRuntime(\n        runtimeRoot = runtimeRoot,\n        scriptFile = File(runtimeRoot, \"web-screenshot.js\"),\n        nodeFile = getBundledNodeExecutable(runtimeRoot),\n        browsersDir = getBundledPlaywrightBrowsersPath(runtimeRoot)\n    )\n}\n\nprivate fun resolveNodeRuntime(): NodeRuntime {\n    val bundledNode = ensureBundledWebRuntime()?.nodeFile\n    return if (bundledNode != null) {\n        logger.info(\"使用内置 Node.js: ${bundledNode.absolutePath}\")\n        NodeRuntime(bundledNode.absolutePath, bundled = true)\n    } else {\n        NodeRuntime(\"node\", bundled = false)\n    }\n}\n\n/**\n * 从剪贴板获取 URL\n */\nfun getUrlFromClipboard(): String? {\n    return try {\n        val clipboard = java.awt.Toolkit.getDefaultToolkit().systemClipboard\n        val contents = clipboard.getContents(null)\n        if (contents != null && contents.isDataFlavorSupported(java.awt.datatransfer.DataFlavor.stringFlavor)) {\n            val text = contents.getTransferData(java.awt.datatransfer.DataFlavor.stringFlavor) as String\n            val trimmed = text.trim()\n            // 检查是否是有效的 URL\n            if (trimmed.startsWith(\"http://\") || trimmed.startsWith(\"https://\")) {\n                logger.info(\"从剪贴板获取到 URL: $trimmed\")\n                trimmed\n            } else {\n                logger.debug(\"剪贴板内容不是有效的 URL: $trimmed\")\n                null\n            }\n        } else {\n            logger.debug(\"剪贴板中没有文本内容\")\n            null\n        }\n    } catch (e: Exception) {\n        logger.error(\"读取剪贴板失败\", e)\n        null\n    }\n}\n\n/**\n * 从剪贴板获取 Cookie\n * 支持两种格式：\n * 1. Netscape Cookie 格式（从浏览器扩展如 EditThisCookie 导出）\n * 2. JSON 格式（Playwright Cookie 格式）\n */\nfun getCookiesFromClipboard(): List<Cookie> {\n    return try {\n        val clipboard = java.awt.Toolkit.getDefaultToolkit().systemClipboard\n        val contents = clipboard.getContents(null)\n        if (contents != null && contents.isDataFlavorSupported(java.awt.datatransfer.DataFlavor.stringFlavor)) {\n            val text = contents.getTransferData(java.awt.datatransfer.DataFlavor.stringFlavor) as String\n            val trimmed = text.trim()\n            \n            // 尝试解析 JSON 格式（Playwright Cookie 格式）\n            if (trimmed.startsWith(\"[\") || trimmed.startsWith(\"{\")) {\n                try {\n                    val jsonArray = JsonParser.parseString(trimmed)\n                    if (jsonArray.isJsonArray) {\n                        val cookies = mutableListOf<Cookie>()\n                        jsonArray.asJsonArray.forEach { element ->\n                            val obj = element.asJsonObject\n                            cookies.add(\n                                Cookie(\n                                    name = obj.get(\"name\")?.asString ?: \"\",\n                                    value = obj.get(\"value\")?.asString ?: \"\",\n                                    domain = obj.get(\"domain\")?.asString,\n                                    path = obj.get(\"path\")?.asString,\n                                    expires = obj.get(\"expires\")?.asLong,\n                                    httpOnly = obj.get(\"httpOnly\")?.asBoolean ?: false,\n                                    secure = obj.get(\"secure\")?.asBoolean ?: false,\n                                    sameSite = obj.get(\"sameSite\")?.asString\n                                )\n                            )\n                        }\n                        logger.info(\"从剪贴板解析到 ${cookies.size} 个 Cookie（JSON 格式）\")\n                        return cookies\n                    }\n                } catch (e: Exception) {\n                    logger.debug(\"解析 JSON Cookie 失败，尝试 Netscape 格式\", e)\n                }\n            }\n            \n            // 尝试解析 Netscape Cookie 格式\n            val cookies = parseNetscapeCookies(trimmed)\n            if (cookies.isNotEmpty()) {\n                logger.info(\"从剪贴板解析到 ${cookies.size} 个 Cookie（Netscape 格式）\")\n                return cookies\n            }\n            \n            emptyList()\n        } else {\n            emptyList()\n        }\n    } catch (e: Exception) {\n        logger.error(\"读取剪贴板 Cookie 失败\", e)\n        emptyList()\n    }\n}\n\n/**\n * 解析 Netscape Cookie 格式\n * 格式：domain\tflag\tpath\tsecure\texpiration\tname\tvalue\n */\nprivate fun parseNetscapeCookies(text: String): List<Cookie> {\n    val cookies = mutableListOf<Cookie>()\n    val lines = text.lines()\n    \n    // 跳过注释行和标题行\n    val dataLines = lines.filter { \n        val trimmed = it.trim()\n        !trimmed.startsWith(\"#\") && \n        trimmed.isNotEmpty() &&\n        (trimmed.contains(\"\\t\") && trimmed.split(\"\\t\").size >= 7)\n    }\n    \n    dataLines.forEach { line ->\n        try {\n            val parts = line.split(\"\\t\")\n            if (parts.size >= 7) {\n                val domain = parts[0].trim()\n                val path = parts[2].trim()\n                val secure = parts[3].trim() == \"TRUE\"\n                val expiration = parts[4].trim().toLongOrNull()\n                val name = parts[5].trim()\n                val value = parts[6].trim()\n                \n                if (name.isNotEmpty() && value.isNotEmpty()) {\n                    cookies.add(\n                        Cookie(\n                            name = name,\n                            value = value,\n                            domain = domain.takeIf { it.isNotEmpty() },\n                            path = path.takeIf { it.isNotEmpty() },\n                            expires = expiration,\n                            secure = secure\n                        )\n                    )\n                }\n            }\n        } catch (e: Exception) {\n            logger.debug(\"解析 Cookie 行失败: $line\", e)\n        }\n    }\n    \n    return cookies\n}\n\n/**\n * 捕获网页长截图\n * \n * @param url 网页URL\n * @param options 截图选项\n * @return 截图结果，失败返回 Result.failure\n */\nsuspend fun captureWebPage(\n    url: String,\n    options: WebScreenshotOptions = WebScreenshotOptions()\n): Result<BufferedImage> = withContext(Dispatchers.IO) {\n    try {\n        val bundledRuntime = ensureBundledWebRuntime()\n\n        // 1. 检查 Node.js 环境\n        val nodeRuntime = resolveNodeRuntime()\n        if (!checkNodeInstalled(nodeRuntime)) {\n            return@withContext Result.failure(\n                IllegalStateException(\n                    if (nodeRuntime.bundled) {\n                        \"内置 Node.js 不可用，请检查安装包中的运行时资源\"\n                    } else {\n                        \"未检测到 Node.js 环境，请先安装 Node.js\"\n                    }\n                )\n            )\n        }\n\n        // 2. 检查脚本文件是否存在\n        val scriptFile = bundledRuntime?.scriptFile\n        if (scriptFile == null || !scriptFile.exists()) {\n            val errorPath = scriptFile?.absolutePath ?: \"未知路径\"\n            return@withContext Result.failure(\n                FileNotFoundException(\"网页截图脚本不存在: $errorPath。请确保 web-screenshot.js 文件在 resources 目录下。\")\n            )\n        }\n\n        // 3. 创建临时文件用于输出图片\n        val tempImageFile = File.createTempFile(\"web-screenshot-\", \".png\")\n        tempImageFile.deleteOnExit()\n\n        // 4. 检查并安装依赖\n        val workingDir = bundledRuntime?.runtimeRoot ?: scriptFile.parentFile ?: File(\".\")\n        val packageJson = File(workingDir, \"package.json\")\n        val nodeModules = File(workingDir, \"node_modules\")\n        \n        // 离线运行时应已自带依赖，这里只做完整性兜底检查\n        if (packageJson.exists() && !nodeModules.exists()) {\n            logger.warn(\"检测到离线网页截图运行时不完整: ${workingDir.absolutePath}\")\n            return@withContext Result.failure(\n                IllegalStateException(\"离线网页截图运行时不完整，请重新安装应用或重新打包\")\n            )\n        }\n        \n        // 5. 如果有 Cookie，保存到临时文件\n        val cookiesFile = if (options.cookies.isNotEmpty()) {\n            val tempCookiesFile = File.createTempFile(\"web-screenshot-cookies-\", \".json\")\n            tempCookiesFile.deleteOnExit()\n            \n            // 转换为 Playwright Cookie 格式\n            val gson = Gson()\n            val cookieList = options.cookies.map { cookie ->\n                val cookieObj = JsonObject()\n                cookieObj.addProperty(\"name\", cookie.name)\n                cookieObj.addProperty(\"value\", cookie.value)\n                cookie.domain?.let { cookieObj.addProperty(\"domain\", it) }\n                cookie.path?.let { cookieObj.addProperty(\"path\", it) }\n                cookie.expires?.let { cookieObj.addProperty(\"expires\", it) }\n                cookieObj.addProperty(\"httpOnly\", cookie.httpOnly)\n                cookieObj.addProperty(\"secure\", cookie.secure)\n                cookie.sameSite?.let { cookieObj.addProperty(\"sameSite\", it) }\n                cookieObj\n            }\n            \n            tempCookiesFile.writeText(gson.toJson(cookieList))\n            logger.info(\"已保存 ${options.cookies.size} 个 Cookie 到临时文件: ${tempCookiesFile.absolutePath}\")\n            tempCookiesFile\n        } else {\n            null\n        }\n\n        // 6. 构建命令\n        val command = mutableListOf<String>().apply {\n            add(nodeRuntime.executable)\n            add(scriptFile.absolutePath)\n            add(url)\n            add(tempImageFile.absolutePath)\n            add(\"--fullPage=${options.fullPage}\")\n            add(\"--waitUntil=${options.waitUntil}\")\n            add(\"--timeout=${options.timeout}\")\n            options.viewportWidth?.let { add(\"--viewportWidth=$it\") }\n            options.viewportHeight?.let { add(\"--viewportHeight=$it\") }\n            add(\"--deviceScaleFactor=${options.deviceScaleFactor}\")\n            cookiesFile?.let { add(\"--cookiesFile=${it.absolutePath}\") }\n        }\n\n        logger.info(\"执行网页截图命令: ${command.joinToString(\" \")}\")\n\n        // 7. 执行进程\n        val processBuilder = ProcessBuilder(command)\n            .directory(workingDir)\n            .redirectErrorStream(true)\n\n        bundledRuntime?.browsersDir?.let { browsersDir ->\n            processBuilder.environment()[\"PLAYWRIGHT_BROWSERS_PATH\"] = browsersDir.absolutePath\n            processBuilder.environment()[\"PLAYWRIGHT_SKIP_BROWSER_GC\"] = \"1\"\n        }\n\n        val process = processBuilder.start()\n\n        // 8. 并发消费输出，避免 stdout 管道写满导致子进程阻塞\n        val output = StringBuffer()\n        val outputReaderThread = thread(start = true, isDaemon = true, name = \"web-screenshot-output-reader\") {\n            try {\n                process.inputStream.bufferedReader().use { reader ->\n                    reader.lineSequence().forEach { line ->\n                        output.appendLine(line)\n                        logger.debug(\"Node输出: $line\")\n                    }\n                }\n            } catch (e: IOException) {\n                logger.debug(\"读取网页截图进程输出结束\", e)\n            }\n        }\n\n        // 9. 等待进程完成（带超时）\n        val processTimeout = options.timeout + 10000 // 额外10秒缓冲\n        val finished = process.waitFor(processTimeout, TimeUnit.MILLISECONDS)\n\n        if (!finished) {\n            process.destroyForcibly()\n            process.inputStream.close()\n            outputReaderThread.join(2000)\n            tempImageFile.delete()\n            cookiesFile?.delete()\n            return@withContext Result.failure(\n                java.util.concurrent.TimeoutException(\"网页截图超时（超过 ${processTimeout}ms）\")\n            )\n        }\n\n        outputReaderThread.join(2000)\n\n        val exitCode = process.exitValue()\n        if (exitCode != 0) {\n            logger.error(\"网页截图失败，退出码: $exitCode，输出: $output\")\n            tempImageFile.delete()\n            cookiesFile?.delete()\n            return@withContext Result.failure(\n                RuntimeException(\"网页截图失败: $output\")\n            )\n        }\n\n        // 10. 清理 Cookie 临时文件\n        cookiesFile?.delete()\n\n        // 11. 读取图片文件\n        if (!tempImageFile.exists() || tempImageFile.length() == 0L) {\n            cookiesFile?.delete()\n            return@withContext Result.failure(\n                IOException(\"截图文件未生成或为空\")\n            )\n        }\n\n        val image = ImageIO.read(tempImageFile)\n        if (image == null) {\n            tempImageFile.delete()\n            cookiesFile?.delete()\n            return@withContext Result.failure(\n                IOException(\"无法读取截图文件\")\n            )\n        }\n\n        // 12. 清理临时文件\n        tempImageFile.delete()\n\n        logger.info(\"网页截图成功，尺寸: ${image.width}x${image.height}\")\n        Result.success(image)\n\n    } catch (e: Exception) {\n        logger.error(\"网页截图异常\", e)\n        Result.failure(e)\n    }\n}\n\n/**\n * 将网页截图加载到 ApplicationState\n */\nfun loadWebScreenshotToState(\n    state: ApplicationState,\n    url: String,\n    options: WebScreenshotOptions = WebScreenshotOptions()\n) {\n    state.scope.launchWithSuspendLoading {\n        val result = captureWebPage(url, options)\n        \n        result.onSuccess { image ->\n            try {\n                logger.info(\"加载网页截图到应用状态\")\n                val currentImage = state.currentImage ?: state.rawImage\n                if (currentImage != null) {\n                    state.addQueue(currentImage)\n                }\n                state.clearImage()\n                state.rawImage = image\n                state.currentImage = image\n                state.rawImageFile = null\n                state.rawImageFormat = null\n            } catch (e: Exception) {\n                logger.error(\"加载网页截图失败\", e)\n                showError(\n                    ErrorType.FILE_IO_ERROR,\n                    ErrorSeverity.MEDIUM,\n                    \"加载截图失败\",\n                    \"加载截图失败: ${e.message}\"\n                )\n            }\n        }.onFailure { error ->\n            logger.error(\"网页截图失败\", error)\n            val userMessage: String = when (error) {\n                is IllegalStateException -> {\n                    if (error.message?.contains(\"Playwright 依赖未安装\") == true) {\n                        error.message ?: \"Playwright 依赖未安装\"\n                    } else {\n                        error.message ?: \"未检测到 Node.js 环境\"\n                    }\n                }\n                is java.util.concurrent.TimeoutException -> \"截图超时，请检查网络连接或稍后重试\"\n                is FileNotFoundException -> \"网页截图脚本不存在，请检查安装\"\n                is RuntimeException -> {\n                    val errorMsg = error.message ?: \"\"\n                    if (errorMsg.contains(\"Cannot find module 'playwright'\")) {\n                        \"Playwright 运行时缺失，请重新安装应用或重新打包离线运行时\"\n                    } else if (errorMsg.contains(\"加载 Cookie 失败\")) {\n                        \"Cookie 加载失败，请检查 Cookie 格式、域名和有效期\"\n                    } else {\n                        \"网页截图失败: $errorMsg\"\n                    }\n                }\n                else -> {\n                    val errorMsg = error.message ?: \"\"\n                    if (errorMsg.contains(\"Cannot find module 'playwright'\")) {\n                        \"Playwright 运行时缺失，请重新安装应用或重新打包离线运行时\"\n                    } else if (errorMsg.contains(\"加载 Cookie 失败\")) {\n                        \"Cookie 加载失败，请检查 Cookie 格式、域名和有效期\"\n                    } else {\n                        \"网页截图失败: $errorMsg\"\n                    }\n                }\n            }\n            showError(\n                ErrorType.NETWORK_ERROR,\n                ErrorSeverity.MEDIUM,\n                \"网页截图失败\",\n                userMessage\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/utils/extensions/Any+Extensions.kt",
    "content": "package cn.netdiscovery.monica.utils.extensions\n\nimport org.slf4j.Logger\nimport org.slf4j.LoggerFactory\nimport kotlin.reflect.full.primaryConstructor\nimport kotlin.reflect.full.memberProperties\nimport kotlin.reflect.jvm.isAccessible\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.utils.extensions.`Any+Extensions`\n * @author: Tony Shen\n * @date: 2025/3/7 14:24\n * @version: V1.0 <描述当前版本功能>\n */\nprivate val logger: Logger = LoggerFactory.getLogger(object : Any() {}.javaClass.enclosingClass)\n\n/**\n * 在构造函数中，打印所有参数的名称、参数值，便于调试\n */\nfun Any.printConstructorParamsWithValues() {\n    val kClass = this::class\n    val constructor = kClass.primaryConstructor\n\n    if (constructor != null) {\n        val paramValues = constructor.parameters.associateWith { param ->\n            val paramName = param.name ?: \"unknown\"\n            val property = kClass.memberProperties.find { it.name == paramName }\n\n            property?.let {\n                it.isAccessible = true  // 允许访问 private 属性\n                it.getter.call(this)\n            }\n        }\n\n        val params = paramValues.map { (param, value) ->  \"${param.name} = $value\"}.joinToString { it }\n        logger.info(\"${kClass.simpleName} parameters: $params\")\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/utils/extensions/Coroutine+Extensions.kt",
    "content": "package cn.netdiscovery.monica.utils.extensions\n\nimport cn.netdiscovery.monica.utils.loadingDisplay\nimport cn.netdiscovery.monica.utils.loadingDisplayWithSuspend\nimport com.safframework.kotlin.coroutines.IO\nimport kotlinx.coroutines.*\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.utils.extensions.`Coroutine+Extensions`\n * @author: Tony Shen\n * @date: 2024/8/28 18:28\n * @version: V1.0 <描述当前版本功能>\n */\nfun CoroutineScope.launchWithLoading(block:()->Unit) {\n\n    this.launch(IO) {\n        loadingDisplay(block)\n    }\n}\n\nfun CoroutineScope.launchWithSuspendLoading(block:suspend ()->Unit): Job {\n\n    return this.launch(IO) {\n        loadingDisplayWithSuspend(block)\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/utils/extensions/DrawScope+Extensions.kt",
    "content": "package cn.netdiscovery.monica.utils.extensions\n\nimport androidx.compose.ui.graphics.drawscope.DrawScope\nimport androidx.compose.ui.graphics.nativeCanvas\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.utils.extensions.`DrawScope+Extensions`\n * @author: Tony Shen\n * @date: 2024/11/25 00:49\n * @version: V1.0 <描述当前版本功能>\n */\n\nfun DrawScope.drawWithLayer(block: DrawScope.() -> Unit) {\n    with(drawContext.canvas.nativeCanvas) {\n        val checkPoint = saveLayer(null, null)\n        block()\n        restoreToCount(checkPoint)\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/utils/extensions/Number+Extensions.kt",
    "content": "package cn.netdiscovery.monica.utils.extensions\n\nimport java.text.DecimalFormat\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.utils.extensions.`Number+Extension`\n * @author: Tony Shen\n * @date:  2024/5/4 14:30\n * @version: V1.0 <描述当前版本功能>\n */\nval format by lazy {\n    DecimalFormat(\"#.##\")\n}\n\nfun Float.to2fStr(): String = format.format(this)"
  },
  {
    "path": "src/jvmMain/kotlin/cn/netdiscovery/monica/utils/extensions/String+Extensions.kt",
    "content": "package cn.netdiscovery.monica.utils.extensions\n\n/**\n *\n * @FileName:\n *          cn.netdiscovery.monica.utils.extensions.`String+Extension`\n * @author: Tony Shen\n * @date:  2024/5/2 15:30\n * @version: V1.0 <描述当前版本功能>\n */\nimport java.net.URL\n\n/**\n * 将 string 字符串安全地转换成 int 类型\n */\nfun String.safelyConvertToInt(): Int? = this.toDoubleOrNull()?.takeIf { it % 1 == 0.0 }?.toInt()\n\n\nfun String.isValidUrl(): Boolean {\n    return try {\n        URL(this).toURI()\n        true\n    } catch (e: Exception) {\n        false\n    }\n}\n"
  },
  {
    "path": "src/jvmMain/resources/logback.xml",
    "content": "<!--每天生成一个文件，归档文件保存30天：-->\n<configuration>\n\n    <define name=\"LOG_HOME\" class=\"cn.netdiscovery.monica.utils.LogHomeProperty\"/>\n\n    <property name=\"pattern\" value=\"%d{HH:mm:ss.SSS} [%-5level] [%thread] [%logger] %msg%n\"/>\n\n    <!-- 控制台输出日志 -->\n    <appender name=\"CONSOLE\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>${pattern}</pattern>\n        </encoder>\n    </appender>\n\n    <!-- 每日滚动日志文件 -->\n    <appender name=\"FILE\" class=\"ch.qos.logback.core.rolling.RollingFileAppender\">\n        <file>${LOG_HOME}monica.log</file>\n        <rollingPolicy class=\"ch.qos.logback.core.rolling.TimeBasedRollingPolicy\">\n            <fileNamePattern>${LOG_HOME}monica_%d{yyyy-MM-dd}.log</fileNamePattern>\n            <maxHistory>30</maxHistory>\n        </rollingPolicy>\n        <encoder>\n            <pattern>${pattern}</pattern>\n        </encoder>\n    </appender>\n\n    <root level=\"INFO\">\n        <appender-ref ref=\"CONSOLE\"/>\n        <appender-ref ref=\"FILE\"/>\n    </root>\n\n</configuration>"
  },
  {
    "path": "src/jvmTest/kotlin/cn/netdiscovery/monica/editor/layer/ExportManagerTest.kt",
    "content": "package cn.netdiscovery.monica.editor.layer\n\nimport androidx.compose.ui.geometry.Rect\nimport androidx.compose.ui.graphics.Canvas\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.ImageBitmap\nimport androidx.compose.ui.graphics.Paint\nimport androidx.compose.ui.graphics.toAwtImage\nimport androidx.compose.ui.graphics.toArgb\nimport androidx.compose.ui.unit.Density\nimport cn.netdiscovery.monica.ui.controlpanel.shapedrawing.EditorController\nimport cn.netdiscovery.monica.ui.controlpanel.shapedrawing.layer.ImageLayer\nimport kotlin.test.Test\nimport kotlin.test.assertEquals\n\nclass EditorControllerExportTest {\n\n    @Test\n    fun `exportImageBitmap composes image layers`() {\n        val editorController = EditorController()\n\n        val redBitmap = createSolidBitmap(Color.Red, 8, 8)\n        val imageLayer = ImageLayer(name = \"背景图层\", image = redBitmap)\n\n        editorController.addLayer(imageLayer)\n\n        val result = editorController.exportImageBitmap(\n            width = 8,\n            height = 8,\n            density = Density(1f)\n        )\n\n        val buffered = result.toAwtImage()\n        val pixel = buffered.getRGB(4, 4)\n        assertEquals(Color.Red.toArgb(), pixel)\n    }\n\n    private fun createSolidBitmap(color: Color, width: Int, height: Int): ImageBitmap {\n        val bitmap = ImageBitmap(width, height)\n        val canvas = Canvas(bitmap)\n        val paint = Paint().apply {\n            this.color = color\n        }\n        canvas.drawRect(Rect(0f, 0f, width.toFloat(), height.toFloat()), paint)\n        return bitmap\n    }\n}\n\n"
  },
  {
    "path": "src/jvmTest/kotlin/cn/netdiscovery/monica/editor/layer/LayerManagerTest.kt",
    "content": "package cn.netdiscovery.monica.editor.layer\n\nimport cn.netdiscovery.monica.ui.controlpanel.shapedrawing.layer.ImageLayer\nimport cn.netdiscovery.monica.ui.controlpanel.shapedrawing.layer.LayerManager\nimport cn.netdiscovery.monica.ui.controlpanel.shapedrawing.layer.ShapeLayer\nimport kotlin.test.Test\nimport kotlin.test.assertEquals\nimport kotlin.test.assertNull\nimport kotlin.test.assertTrue\n\nclass LayerManagerTest {\n\n    @Test\n    fun `add layer updates active layer`() {\n        val manager = LayerManager()\n        val layer = ImageLayer(name = \"Background\", image = null)\n\n        manager.addLayer(layer)\n\n        assertEquals(1, manager.layers.value.size)\n        assertEquals(layer.id, manager.activeLayer.value?.id)\n    }\n\n    @Test\n    fun `remove active layer selects previous`() {\n        val manager = LayerManager()\n        val first = ImageLayer(name = \"Layer 1\", image = null)\n        val second = ShapeLayer(name = \"Layer 2\")\n\n        manager.addLayer(first)\n        manager.addLayer(second)\n\n        assertEquals(second.id, manager.activeLayer.value?.id)\n\n        manager.removeLayer(second.id)\n\n        assertEquals(1, manager.layers.value.size)\n        assertEquals(first.id, manager.activeLayer.value?.id)\n    }\n\n    @Test\n    fun `move layer up swaps ordering`() {\n        val manager = LayerManager()\n        val bottom = ShapeLayer(\"Bottom\")\n        val top = ShapeLayer(\"Top\")\n\n        manager.addLayer(bottom)\n        manager.addLayer(top)\n\n        assertEquals(listOf(bottom, top), manager.layers.value)\n\n        manager.moveLayerUp(bottom.id)\n\n        assertEquals(listOf(top, bottom), manager.layers.value)\n    }\n\n    @Test\n    fun `clear removes all layers`() {\n        val manager = LayerManager()\n        manager.addLayer(ImageLayer(\"A\", null))\n        manager.addLayer(ImageLayer(\"B\", null))\n\n        manager.clear()\n\n        assertTrue(manager.layers.value.isEmpty())\n        assertNull(manager.activeLayer.value)\n    }\n}\n\n\n\n"
  }
]