Compare commits
33 Commits
a588caf743
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0541dbfd69 | ||
|
|
52fb288bed | ||
|
|
2ce9e5471e | ||
|
|
2c89ef42b3 | ||
|
|
330a62027c | ||
|
|
302f106ae6 | ||
|
|
3ed264f349 | ||
|
|
99c442663a | ||
|
|
a01277a11c | ||
|
|
0cf429fff2 | ||
|
|
d25f87abe0 | ||
|
|
84ae92d1e3 | ||
|
|
6cb5b7a61f | ||
|
|
eb251b5eac | ||
|
|
6439759b04 | ||
|
|
f96033e421 | ||
|
|
64d6e770ad | ||
|
|
3205c744cf | ||
|
|
0b8f060c6f | ||
|
|
2a468518af | ||
|
|
713f0885dc | ||
|
|
e1f2c8efc8 | ||
|
|
16bb3e5135 | ||
|
|
358aae92dc | ||
|
|
fd90ac1df3 | ||
|
|
3e5d5aa848 | ||
|
|
de0ed2bdc2 | ||
|
|
f34021eae1 | ||
|
|
a0822153c1 | ||
|
|
54408a5933 | ||
|
|
a5f1b165fd | ||
|
|
0c307c319a | ||
|
|
0c5824d85c |
3
.gitignore
vendored
@@ -22,3 +22,6 @@ dist-ssr
|
|||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
|
||||||
|
*.onnx
|
||||||
|
*.dll
|
||||||
202
LICENSE.txt
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
10
README.md
@@ -1,7 +1,7 @@
|
|||||||
# Tauri + Vue + TypeScript
|
# Watermark Wizard
|
||||||
|
|
||||||
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
Generated by Gemini
|
||||||
|
|
||||||
## Recommended IDE Setup
|
Models:
|
||||||
|
- LaMa: https://huggingface.co/Carve/LaMa-ONNX/blob/main/lama_fp32.onnx
|
||||||
- [VS Code](https://code.visualstudio.com/) + [Vue - Official](https://marketplace.visualstudio.com/items?itemName=Vue.volar) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)
|
- en_PP-OCRv3_det_infer: https://huggingface.co/SWHL/RapidOCR/blob/main/PP-OCRv4/en_PP-OCRv3_det_infer.onnx
|
||||||
|
|||||||
BIN
app-icon.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Tauri + Vue + Typescript App</title>
|
<title>Watermark Wizard</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "watermark-wizard",
|
"name": "watermark-wizard",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.0",
|
"version": "1.0.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
"@tauri-apps/api": "^2",
|
"@tauri-apps/api": "^2",
|
||||||
"@tauri-apps/plugin-dialog": "^2.6.0",
|
"@tauri-apps/plugin-dialog": "^2.6.0",
|
||||||
"autoprefixer": "^10.4.23",
|
"autoprefixer": "^10.4.23",
|
||||||
|
"lucide-vue-next": "^0.562.0",
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"tailwindcss": "^4.1.18",
|
"tailwindcss": "^4.1.18",
|
||||||
|
|||||||
12
pnpm-lock.yaml
generated
@@ -17,6 +17,9 @@ importers:
|
|||||||
autoprefixer:
|
autoprefixer:
|
||||||
specifier: ^10.4.23
|
specifier: ^10.4.23
|
||||||
version: 10.4.23(postcss@8.5.6)
|
version: 10.4.23(postcss@8.5.6)
|
||||||
|
lucide-vue-next:
|
||||||
|
specifier: ^0.562.0
|
||||||
|
version: 0.562.0(vue@3.5.26(typescript@5.6.3))
|
||||||
pinia:
|
pinia:
|
||||||
specifier: ^3.0.4
|
specifier: ^3.0.4
|
||||||
version: 3.0.4(typescript@5.6.3)(vue@3.5.26(typescript@5.6.3))
|
version: 3.0.4(typescript@5.6.3)(vue@3.5.26(typescript@5.6.3))
|
||||||
@@ -782,6 +785,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==}
|
resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==}
|
||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
|
|
||||||
|
lucide-vue-next@0.562.0:
|
||||||
|
resolution: {integrity: sha512-LN0BLGKMFulv0lnfK29r14DcngRUhIqdcaL0zXTt2o0oS9odlrjCGaU3/X9hIihOjjN8l8e+Y9G/famcNYaI7Q==}
|
||||||
|
peerDependencies:
|
||||||
|
vue: '>=3.0.1'
|
||||||
|
|
||||||
magic-string@0.30.21:
|
magic-string@0.30.21:
|
||||||
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
||||||
|
|
||||||
@@ -1524,6 +1532,10 @@ snapshots:
|
|||||||
lightningcss-win32-arm64-msvc: 1.30.2
|
lightningcss-win32-arm64-msvc: 1.30.2
|
||||||
lightningcss-win32-x64-msvc: 1.30.2
|
lightningcss-win32-x64-msvc: 1.30.2
|
||||||
|
|
||||||
|
lucide-vue-next@0.562.0(vue@3.5.26(typescript@5.6.3)):
|
||||||
|
dependencies:
|
||||||
|
vue: 3.5.26(typescript@5.6.3)
|
||||||
|
|
||||||
magic-string@0.30.21:
|
magic-string@0.30.21:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
|
|||||||
BIN
public/fonts/Roboto-Regular.ttf
Normal file
557
src-tauri/Cargo.lock
generated
@@ -2,6 +2,22 @@
|
|||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 4
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ab_glyph"
|
||||||
|
version = "0.2.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "01c0457472c38ea5bd1c3b5ada5e368271cb550be7a4ca4a0b4634e9913f6cc2"
|
||||||
|
dependencies = [
|
||||||
|
"ab_glyph_rasterizer",
|
||||||
|
"owned_ttf_parser",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ab_glyph_rasterizer"
|
||||||
|
version = "0.1.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "adler2"
|
name = "adler2"
|
||||||
version = "2.0.1"
|
version = "2.0.1"
|
||||||
@@ -65,6 +81,15 @@ version = "1.0.100"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "approx"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6"
|
||||||
|
dependencies = [
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "arbitrary"
|
name = "arbitrary"
|
||||||
version = "1.4.2"
|
version = "1.4.2"
|
||||||
@@ -187,6 +212,12 @@ version = "0.22.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base64ct"
|
||||||
|
version = "1.8.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bit_field"
|
name = "bit_field"
|
||||||
version = "0.10.3"
|
version = "0.10.3"
|
||||||
@@ -451,6 +482,16 @@ dependencies = [
|
|||||||
"version_check",
|
"version_check",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "core-foundation"
|
||||||
|
version = "0.9.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
|
||||||
|
dependencies = [
|
||||||
|
"core-foundation-sys",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "core-foundation"
|
name = "core-foundation"
|
||||||
version = "0.10.1"
|
version = "0.10.1"
|
||||||
@@ -474,9 +515,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1"
|
checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.10.0",
|
"bitflags 2.10.0",
|
||||||
"core-foundation",
|
"core-foundation 0.10.1",
|
||||||
"core-graphics-types",
|
"core-graphics-types",
|
||||||
"foreign-types",
|
"foreign-types 0.5.0",
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -487,7 +528,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb"
|
checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.10.0",
|
"bitflags 2.10.0",
|
||||||
"core-foundation",
|
"core-foundation 0.10.1",
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -640,6 +681,16 @@ dependencies = [
|
|||||||
"syn 2.0.114",
|
"syn 2.0.114",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "der"
|
||||||
|
version = "0.7.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
|
||||||
|
dependencies = [
|
||||||
|
"pem-rfc7468",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "deranged"
|
name = "deranged"
|
||||||
version = "0.5.5"
|
version = "0.5.5"
|
||||||
@@ -845,6 +896,16 @@ dependencies = [
|
|||||||
"typeid",
|
"typeid",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "errno"
|
||||||
|
version = "0.3.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "exr"
|
name = "exr"
|
||||||
version = "1.74.0"
|
version = "1.74.0"
|
||||||
@@ -860,6 +921,12 @@ dependencies = [
|
|||||||
"zune-inflate",
|
"zune-inflate",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fastrand"
|
||||||
|
version = "2.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fax"
|
name = "fax"
|
||||||
version = "0.2.6"
|
version = "0.2.6"
|
||||||
@@ -921,6 +988,15 @@ version = "1.0.7"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "foreign-types"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
|
||||||
|
dependencies = [
|
||||||
|
"foreign-types-shared 0.1.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "foreign-types"
|
name = "foreign-types"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
@@ -928,7 +1004,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
|
checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"foreign-types-macros",
|
"foreign-types-macros",
|
||||||
"foreign-types-shared",
|
"foreign-types-shared 0.3.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -942,6 +1018,12 @@ dependencies = [
|
|||||||
"syn 2.0.114",
|
"syn 2.0.114",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "foreign-types-shared"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "foreign-types-shared"
|
name = "foreign-types-shared"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
@@ -1175,8 +1257,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
|
"js-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"wasi 0.11.1+wasi-snapshot-preview1",
|
"wasi 0.11.1+wasi-snapshot-preview1",
|
||||||
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1390,6 +1474,12 @@ version = "0.4.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hmac-sha256"
|
||||||
|
version = "1.1.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ad6880c8d4a9ebf39c6e8b77007ce223f646a4d21ce29d99f70cb16420545425"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "html5ever"
|
name = "html5ever"
|
||||||
version = "0.29.1"
|
version = "0.29.1"
|
||||||
@@ -1668,6 +1758,24 @@ dependencies = [
|
|||||||
"quick-error",
|
"quick-error",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "imageproc"
|
||||||
|
version = "0.25.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2393fb7808960751a52e8a154f67e7dd3f8a2ef9bd80d1553078a7b4e8ed3f0d"
|
||||||
|
dependencies = [
|
||||||
|
"ab_glyph",
|
||||||
|
"approx",
|
||||||
|
"getrandom 0.2.17",
|
||||||
|
"image",
|
||||||
|
"itertools 0.12.1",
|
||||||
|
"nalgebra",
|
||||||
|
"num",
|
||||||
|
"rand 0.8.5",
|
||||||
|
"rand_distr",
|
||||||
|
"rayon",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "imgref"
|
name = "imgref"
|
||||||
version = "1.12.0"
|
version = "1.12.0"
|
||||||
@@ -1733,6 +1841,15 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itertools"
|
||||||
|
version = "0.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
|
||||||
|
dependencies = [
|
||||||
|
"either",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itertools"
|
name = "itertools"
|
||||||
version = "0.14.0"
|
version = "0.14.0"
|
||||||
@@ -1890,7 +2007,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf"
|
checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gtk-sys",
|
"gtk-sys",
|
||||||
"libloading",
|
"libloading 0.7.4",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1920,6 +2037,22 @@ dependencies = [
|
|||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libloading"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "754ca22de805bb5744484a5b151a9e1a8e837d5dc232c2d7d8c2e3492edc8b60"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"windows-link 0.2.1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libm"
|
||||||
|
version = "0.2.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libredox"
|
name = "libredox"
|
||||||
version = "0.1.12"
|
version = "0.1.12"
|
||||||
@@ -1930,6 +2063,12 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "linux-raw-sys"
|
||||||
|
version = "0.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "litemap"
|
name = "litemap"
|
||||||
version = "0.8.1"
|
version = "0.8.1"
|
||||||
@@ -1960,6 +2099,12 @@ dependencies = [
|
|||||||
"imgref",
|
"imgref",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lzma-rust2"
|
||||||
|
version = "0.15.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1670343e58806300d87950e3401e820b519b9384281bbabfb15e3636689ffd69"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mac"
|
name = "mac"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
@@ -1997,6 +2142,16 @@ version = "0.1.10"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
|
checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "matrixmultiply"
|
||||||
|
version = "0.3.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
"rawpointer",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "maybe-rayon"
|
name = "maybe-rayon"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
@@ -2080,6 +2235,68 @@ dependencies = [
|
|||||||
"windows-sys 0.60.2",
|
"windows-sys 0.60.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nalgebra"
|
||||||
|
version = "0.32.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7b5c17de023a86f59ed79891b2e5d5a94c705dbe904a5b5c9c952ea6221b03e4"
|
||||||
|
dependencies = [
|
||||||
|
"approx",
|
||||||
|
"matrixmultiply",
|
||||||
|
"num-complex",
|
||||||
|
"num-rational",
|
||||||
|
"num-traits",
|
||||||
|
"simba",
|
||||||
|
"typenum",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "native-tls"
|
||||||
|
version = "0.2.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"log",
|
||||||
|
"openssl",
|
||||||
|
"openssl-probe",
|
||||||
|
"openssl-sys",
|
||||||
|
"schannel",
|
||||||
|
"security-framework",
|
||||||
|
"security-framework-sys",
|
||||||
|
"tempfile",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ndarray"
|
||||||
|
version = "0.16.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "882ed72dce9365842bf196bdeedf5055305f11fc8c03dee7bb0194a6cad34841"
|
||||||
|
dependencies = [
|
||||||
|
"matrixmultiply",
|
||||||
|
"num-complex",
|
||||||
|
"num-integer",
|
||||||
|
"num-traits",
|
||||||
|
"portable-atomic",
|
||||||
|
"portable-atomic-util",
|
||||||
|
"rawpointer",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ndarray"
|
||||||
|
version = "0.17.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "520080814a7a6b4a6e9070823bb24b4531daac8c4627e08ba5de8c5ef2f2752d"
|
||||||
|
dependencies = [
|
||||||
|
"matrixmultiply",
|
||||||
|
"num-complex",
|
||||||
|
"num-integer",
|
||||||
|
"num-traits",
|
||||||
|
"portable-atomic",
|
||||||
|
"portable-atomic-util",
|
||||||
|
"rawpointer",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ndk"
|
name = "ndk"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
@@ -2137,6 +2354,20 @@ version = "0.3.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
|
checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num"
|
||||||
|
version = "0.4.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23"
|
||||||
|
dependencies = [
|
||||||
|
"num-bigint",
|
||||||
|
"num-complex",
|
||||||
|
"num-integer",
|
||||||
|
"num-iter",
|
||||||
|
"num-rational",
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-bigint"
|
name = "num-bigint"
|
||||||
version = "0.4.6"
|
version = "0.4.6"
|
||||||
@@ -2147,6 +2378,15 @@ dependencies = [
|
|||||||
"num-traits",
|
"num-traits",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-complex"
|
||||||
|
version = "0.4.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
|
||||||
|
dependencies = [
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-conv"
|
name = "num-conv"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -2173,6 +2413,17 @@ dependencies = [
|
|||||||
"num-traits",
|
"num-traits",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-iter"
|
||||||
|
version = "0.1.45"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
"num-integer",
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-rational"
|
name = "num-rational"
|
||||||
version = "0.4.2"
|
version = "0.4.2"
|
||||||
@@ -2191,6 +2442,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"autocfg",
|
"autocfg",
|
||||||
|
"libm",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2433,12 +2685,90 @@ version = "1.21.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "openssl"
|
||||||
|
version = "0.10.75"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.10.0",
|
||||||
|
"cfg-if",
|
||||||
|
"foreign-types 0.3.2",
|
||||||
|
"libc",
|
||||||
|
"once_cell",
|
||||||
|
"openssl-macros",
|
||||||
|
"openssl-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "openssl-macros"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.114",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "openssl-probe"
|
||||||
|
version = "0.1.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "openssl-sys"
|
||||||
|
version = "0.9.111"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"libc",
|
||||||
|
"pkg-config",
|
||||||
|
"vcpkg",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "option-ext"
|
name = "option-ext"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ort"
|
||||||
|
version = "2.0.0-rc.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4a5df903c0d2c07b56950f1058104ab0c8557159f2741782223704de9be73c3c"
|
||||||
|
dependencies = [
|
||||||
|
"libloading 0.9.0",
|
||||||
|
"ndarray 0.17.2",
|
||||||
|
"ort-sys",
|
||||||
|
"smallvec",
|
||||||
|
"tracing",
|
||||||
|
"ureq",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ort-sys"
|
||||||
|
version = "2.0.0-rc.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "06503bb33f294c5f1ba484011e053bfa6ae227074bdb841e9863492dc5960d4b"
|
||||||
|
dependencies = [
|
||||||
|
"hmac-sha256",
|
||||||
|
"lzma-rust2",
|
||||||
|
"ureq",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "owned_ttf_parser"
|
||||||
|
version = "0.25.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b"
|
||||||
|
dependencies = [
|
||||||
|
"ttf-parser",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pango"
|
name = "pango"
|
||||||
version = "0.18.3"
|
version = "0.18.3"
|
||||||
@@ -2499,6 +2829,15 @@ version = "0.1.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec"
|
checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pem-rfc7468"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412"
|
||||||
|
dependencies = [
|
||||||
|
"base64ct",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "percent-encoding"
|
name = "percent-encoding"
|
||||||
version = "2.3.2"
|
version = "2.3.2"
|
||||||
@@ -2696,6 +3035,21 @@ dependencies = [
|
|||||||
"miniz_oxide",
|
"miniz_oxide",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "portable-atomic"
|
||||||
|
version = "1.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "portable-atomic-util"
|
||||||
|
version = "0.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507"
|
||||||
|
dependencies = [
|
||||||
|
"portable-atomic",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "potential_utf"
|
name = "potential_utf"
|
||||||
version = "0.1.4"
|
version = "0.1.4"
|
||||||
@@ -2953,6 +3307,16 @@ dependencies = [
|
|||||||
"getrandom 0.3.4",
|
"getrandom 0.3.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_distr"
|
||||||
|
version = "0.4.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31"
|
||||||
|
dependencies = [
|
||||||
|
"num-traits",
|
||||||
|
"rand 0.8.5",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand_hc"
|
name = "rand_hc"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -2987,7 +3351,7 @@ dependencies = [
|
|||||||
"built",
|
"built",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"interpolate_name",
|
"interpolate_name",
|
||||||
"itertools",
|
"itertools 0.14.0",
|
||||||
"libc",
|
"libc",
|
||||||
"libfuzzer-sys",
|
"libfuzzer-sys",
|
||||||
"log",
|
"log",
|
||||||
@@ -3027,6 +3391,12 @@ version = "0.6.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
|
checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rawpointer"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rayon"
|
name = "rayon"
|
||||||
version = "1.11.0"
|
version = "1.11.0"
|
||||||
@@ -3190,6 +3560,28 @@ dependencies = [
|
|||||||
"semver",
|
"semver",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustix"
|
||||||
|
version = "1.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.10.0",
|
||||||
|
"errno",
|
||||||
|
"libc",
|
||||||
|
"linux-raw-sys",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls-pki-types"
|
||||||
|
version = "1.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd"
|
||||||
|
dependencies = [
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustversion"
|
name = "rustversion"
|
||||||
version = "1.0.22"
|
version = "1.0.22"
|
||||||
@@ -3202,6 +3594,15 @@ version = "1.0.22"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984"
|
checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "safe_arch"
|
||||||
|
version = "0.7.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "96b02de82ddbe1b636e6170c21be622223aea188ef2e139be0a5b219ec215323"
|
||||||
|
dependencies = [
|
||||||
|
"bytemuck",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "same-file"
|
name = "same-file"
|
||||||
version = "1.0.6"
|
version = "1.0.6"
|
||||||
@@ -3211,6 +3612,15 @@ dependencies = [
|
|||||||
"winapi-util",
|
"winapi-util",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "schannel"
|
||||||
|
version = "0.1.28"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "schemars"
|
name = "schemars"
|
||||||
version = "0.8.22"
|
version = "0.8.22"
|
||||||
@@ -3268,6 +3678,29 @@ version = "1.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "security-framework"
|
||||||
|
version = "2.11.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.10.0",
|
||||||
|
"core-foundation 0.9.4",
|
||||||
|
"core-foundation-sys",
|
||||||
|
"libc",
|
||||||
|
"security-framework-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "security-framework-sys"
|
||||||
|
version = "2.15.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0"
|
||||||
|
dependencies = [
|
||||||
|
"core-foundation-sys",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "selectors"
|
name = "selectors"
|
||||||
version = "0.24.0"
|
version = "0.24.0"
|
||||||
@@ -3483,6 +3916,19 @@ version = "1.3.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "simba"
|
||||||
|
version = "0.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "061507c94fc6ab4ba1c9a0305018408e312e17c041eb63bef8aa726fa33aceae"
|
||||||
|
dependencies = [
|
||||||
|
"approx",
|
||||||
|
"num-complex",
|
||||||
|
"num-traits",
|
||||||
|
"paste",
|
||||||
|
"wide",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "simd-adler32"
|
name = "simd-adler32"
|
||||||
version = "0.3.8"
|
version = "0.3.8"
|
||||||
@@ -3532,6 +3978,17 @@ dependencies = [
|
|||||||
"windows-sys 0.60.2",
|
"windows-sys 0.60.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "socks"
|
||||||
|
version = "0.3.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f0c3dbbd9ae980613c6dd8e28a9407b50509d3803b57624d5dfe8315218cd58b"
|
||||||
|
dependencies = [
|
||||||
|
"byteorder",
|
||||||
|
"libc",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "softbuffer"
|
name = "softbuffer"
|
||||||
version = "0.4.8"
|
version = "0.4.8"
|
||||||
@@ -3691,7 +4148,7 @@ checksum = "f3a753bdc39c07b192151523a3f77cd0394aa75413802c883a0f6f6a0e5ee2e7"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.10.0",
|
"bitflags 2.10.0",
|
||||||
"block2",
|
"block2",
|
||||||
"core-foundation",
|
"core-foundation 0.10.1",
|
||||||
"core-graphics",
|
"core-graphics",
|
||||||
"crossbeam-channel",
|
"crossbeam-channel",
|
||||||
"dispatch",
|
"dispatch",
|
||||||
@@ -4013,6 +4470,19 @@ dependencies = [
|
|||||||
"toml 0.9.11+spec-1.1.0",
|
"toml 0.9.11+spec-1.1.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tempfile"
|
||||||
|
version = "3.24.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c"
|
||||||
|
dependencies = [
|
||||||
|
"fastrand",
|
||||||
|
"getrandom 0.3.4",
|
||||||
|
"once_cell",
|
||||||
|
"rustix",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tendril"
|
name = "tendril"
|
||||||
version = "0.4.3"
|
version = "0.4.3"
|
||||||
@@ -4334,6 +4804,12 @@ version = "0.2.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ttf-parser"
|
||||||
|
version = "0.25.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typeid"
|
name = "typeid"
|
||||||
version = "1.0.3"
|
version = "1.0.3"
|
||||||
@@ -4399,6 +4875,36 @@ version = "1.12.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ureq"
|
||||||
|
version = "3.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d39cb1dbab692d82a977c0392ffac19e188bd9186a9f32806f0aaa859d75585a"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.22.1",
|
||||||
|
"der",
|
||||||
|
"log",
|
||||||
|
"native-tls",
|
||||||
|
"percent-encoding",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"socks",
|
||||||
|
"ureq-proto",
|
||||||
|
"utf-8",
|
||||||
|
"webpki-root-certs",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ureq-proto"
|
||||||
|
version = "0.5.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d81f9efa9df032be5934a46a068815a10a042b494b6a58cb0a1a97bb5467ed6f"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.22.1",
|
||||||
|
"http",
|
||||||
|
"httparse",
|
||||||
|
"log",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "url"
|
name = "url"
|
||||||
version = "2.5.8"
|
version = "2.5.8"
|
||||||
@@ -4459,6 +4965,12 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "vcpkg"
|
||||||
|
version = "0.2.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "version-compare"
|
name = "version-compare"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
@@ -4605,9 +5117,13 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "watermark-wizard"
|
name = "watermark-wizard"
|
||||||
version = "0.1.0"
|
version = "1.0.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"ab_glyph",
|
||||||
"image",
|
"image",
|
||||||
|
"imageproc",
|
||||||
|
"ndarray 0.16.1",
|
||||||
|
"ort",
|
||||||
"rayon",
|
"rayon",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -4670,6 +5186,15 @@ dependencies = [
|
|||||||
"system-deps",
|
"system-deps",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "webpki-root-certs"
|
||||||
|
version = "1.0.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "36a29fc0408b113f68cf32637857ab740edfafdf460c326cd2afaa2d84cc05dc"
|
||||||
|
dependencies = [
|
||||||
|
"rustls-pki-types",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "webview2-com"
|
name = "webview2-com"
|
||||||
version = "0.38.2"
|
version = "0.38.2"
|
||||||
@@ -4712,6 +5237,16 @@ version = "0.1.12"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
|
checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wide"
|
||||||
|
version = "0.7.33"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0ce5da8ecb62bcd8ec8b7ea19f69a51275e91299be594ea5cc6ef7819e16cd03"
|
||||||
|
dependencies = [
|
||||||
|
"bytemuck",
|
||||||
|
"safe_arch",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winapi"
|
name = "winapi"
|
||||||
version = "0.3.9"
|
version = "0.3.9"
|
||||||
@@ -5313,6 +5848,12 @@ dependencies = [
|
|||||||
"synstructure",
|
"synstructure",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zeroize"
|
||||||
|
version = "1.8.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerotrie"
|
name = "zerotrie"
|
||||||
version = "0.2.3"
|
version = "0.2.3"
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "watermark-wizard"
|
name = "watermark-wizard"
|
||||||
version = "0.1.0"
|
version = "1.0.1"
|
||||||
description = "A Tauri App"
|
description = "A Tauri App handles watermarks"
|
||||||
authors = ["you"]
|
authors = ["Julian"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
@@ -21,7 +21,11 @@ tauri-build = { version = "2", features = [] }
|
|||||||
tauri = { version = "2", features = ["protocol-asset"] }
|
tauri = { version = "2", features = ["protocol-asset"] }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
image = "0.25"
|
image = { version = "0.25", features = ["png", "jpeg", "webp"] }
|
||||||
rayon = "1.10"
|
rayon = "1.10"
|
||||||
tauri-plugin-dialog = "2"
|
tauri-plugin-dialog = "2"
|
||||||
|
imageproc = "0.25"
|
||||||
|
ab_glyph = "0.2.23"
|
||||||
|
ort = { version = "=2.0.0-rc.11", features = ["load-dynamic", "directml"] }
|
||||||
|
ndarray = "0.16"
|
||||||
|
|
||||||
|
|||||||
BIN
src-tauri/assets/fonts/Roboto-Regular.ttf
Normal file
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 974 B After Width: | Height: | Size: 1.5 KiB |
BIN
src-tauri/icons/64x64.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 9.5 KiB |
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 903 B After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 2.5 KiB |
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
|
</adaptive-icon>
|
||||||
BIN
src-tauri/icons/android/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
src-tauri/icons/android/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
BIN
src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 9.4 KiB |
BIN
src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
BIN
src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 30 KiB |
BIN
src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="ic_launcher_background">#fff</color>
|
||||||
|
</resources>
|
||||||
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 37 KiB |
BIN
src-tauri/icons/ios/AppIcon-20x20@1x.png
Normal file
|
After Width: | Height: | Size: 793 B |
BIN
src-tauri/icons/ios/AppIcon-20x20@2x-1.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
src-tauri/icons/ios/AppIcon-20x20@2x.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
src-tauri/icons/ios/AppIcon-20x20@3x.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@1x.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@2x-1.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@2x.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@3x.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@1x.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@2x-1.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@2x.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@3x.png
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
BIN
src-tauri/icons/ios/AppIcon-512@2x.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
src-tauri/icons/ios/AppIcon-60x60@2x.png
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
BIN
src-tauri/icons/ios/AppIcon-60x60@3x.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
src-tauri/icons/ios/AppIcon-76x76@1x.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
src-tauri/icons/ios/AppIcon-76x76@2x.png
Normal file
|
After Width: | Height: | Size: 8.6 KiB |
BIN
src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png
Normal file
|
After Width: | Height: | Size: 9.4 KiB |
133
src-tauri/src/lama.rs
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
use crate::ort_ops;
|
||||||
|
use image::{Rgba, RgbaImage};
|
||||||
|
use ort::value::Value;
|
||||||
|
|
||||||
|
pub fn run_lama_inpainting(
|
||||||
|
model_path: &std::path::Path,
|
||||||
|
input_image: &RgbaImage,
|
||||||
|
mask_image: &image::GrayImage,
|
||||||
|
) -> Result<RgbaImage, String> {
|
||||||
|
// 1. Initialize Session
|
||||||
|
let mut session = ort_ops::create_session(model_path)
|
||||||
|
.map_err(|e| format!("Failed to create ORT session for LAMA: {}", e))?;
|
||||||
|
|
||||||
|
// 2. Preprocess
|
||||||
|
let target_size = (512, 512);
|
||||||
|
|
||||||
|
let resized_img = image::imageops::resize(input_image, target_size.0, target_size.1, image::imageops::FilterType::Triangle);
|
||||||
|
let resized_mask = image::imageops::resize(mask_image, target_size.0, target_size.1, image::imageops::FilterType::Triangle);
|
||||||
|
|
||||||
|
// Flatten Image to Vec<f32> (NCHW: 1, 3, 512, 512)
|
||||||
|
let channel_stride = (target_size.0 * target_size.1) as usize;
|
||||||
|
let mut input_data: Vec<f32> = Vec::with_capacity(1 * 3 * channel_stride);
|
||||||
|
|
||||||
|
// We need to fill R plane, then G plane, then B plane
|
||||||
|
let mut r_plane: Vec<f32> = Vec::with_capacity(channel_stride);
|
||||||
|
let mut g_plane: Vec<f32> = Vec::with_capacity(channel_stride);
|
||||||
|
let mut b_plane: Vec<f32> = Vec::with_capacity(channel_stride);
|
||||||
|
|
||||||
|
for (_x, _y, pixel) in resized_img.enumerate_pixels() {
|
||||||
|
r_plane.push(pixel[0] as f32 / 255.0f32);
|
||||||
|
g_plane.push(pixel[1] as f32 / 255.0f32);
|
||||||
|
b_plane.push(pixel[2] as f32 / 255.0f32);
|
||||||
|
}
|
||||||
|
|
||||||
|
input_data.extend(r_plane);
|
||||||
|
input_data.extend(g_plane);
|
||||||
|
input_data.extend(b_plane);
|
||||||
|
|
||||||
|
// Flatten Mask to Vec<f32> (NCHW: 1, 1, 512, 512)
|
||||||
|
let mut mask_data: Vec<f32> = Vec::with_capacity(channel_stride);
|
||||||
|
for (_x, _y, pixel) in resized_mask.enumerate_pixels() {
|
||||||
|
let val = if pixel[0] > 127 { 1.0f32 } else { 0.0f32 };
|
||||||
|
mask_data.push(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Inference
|
||||||
|
// Use (Shape, Data) tuple which implements OwnedTensorArrayData
|
||||||
|
// Explicitly casting shape to i64 is correct for ORT
|
||||||
|
let input_shape = vec![1, 3, target_size.1 as i64, target_size.0 as i64];
|
||||||
|
let input_value = Value::from_array((input_shape, input_data))
|
||||||
|
.map_err(|e| format!("Failed to create input tensor: {}", e))?;
|
||||||
|
|
||||||
|
let mask_shape = vec![1, 1, target_size.1 as i64, target_size.0 as i64];
|
||||||
|
let mask_value = Value::from_array((mask_shape, mask_data))
|
||||||
|
.map_err(|e| format!("Failed to create mask tensor: {}", e))?;
|
||||||
|
|
||||||
|
let inputs = ort::inputs![
|
||||||
|
"image" => input_value,
|
||||||
|
"mask" => mask_value
|
||||||
|
];
|
||||||
|
|
||||||
|
let outputs = session.run(inputs).map_err(|e| format!("Inference failed: {}", e))?;
|
||||||
|
|
||||||
|
// Get output tensor
|
||||||
|
// Just take the first output.
|
||||||
|
let output_tensor_ref = outputs.values().next()
|
||||||
|
.ok_or("No output tensor produced by model")?;
|
||||||
|
|
||||||
|
let (shape, data) = output_tensor_ref.try_extract_tensor::<f32>()
|
||||||
|
.map_err(|e| format!("Failed to extract tensor: {}", e))?;
|
||||||
|
|
||||||
|
// 4. Post-process
|
||||||
|
let mut output_img_512 = RgbaImage::new(target_size.0, target_size.1);
|
||||||
|
|
||||||
|
if shape.len() < 4 {
|
||||||
|
return Err(format!("Unexpected output shape: {:?}", shape));
|
||||||
|
}
|
||||||
|
|
||||||
|
let h = 512;
|
||||||
|
let w = 512;
|
||||||
|
let channel_stride = (h * w) as usize;
|
||||||
|
|
||||||
|
// Safety check on data length
|
||||||
|
if data.len() < (3 * h * w) as usize {
|
||||||
|
return Err(format!("Output data size mismatch. Expected {}, got {}", 3*h*w, data.len()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-detect output range
|
||||||
|
// If values are already in 0-255 range, multiplying by 255 results in all white image.
|
||||||
|
let mut max_val = 0.0f32;
|
||||||
|
// Check a subset of pixels to avoid iterating everything if speed is key, but full scan is safer and fast enough.
|
||||||
|
for v in data.iter().take(1000) {
|
||||||
|
if *v > max_val { max_val = *v; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Heuristic: if max > 2.0, it's likely 0-255. If it's <= 1.0 (or slightly above due to overshoot), it's 0-1.
|
||||||
|
// LaMa usually outputs -1..1 or 0..1. But some exports differ.
|
||||||
|
// Let's assume if any value is > 5.0, it is definitely not 0-1 normalized.
|
||||||
|
let scale_factor = if max_val > 2.0 { 1.0 } else { 255.0 };
|
||||||
|
|
||||||
|
for y in 0..h {
|
||||||
|
for x in 0..w {
|
||||||
|
let offset = (y * w + x) as usize;
|
||||||
|
|
||||||
|
let r_idx = offset;
|
||||||
|
let g_idx = offset + channel_stride;
|
||||||
|
let b_idx = offset + 2 * channel_stride;
|
||||||
|
|
||||||
|
let r = (data[r_idx] * scale_factor).clamp(0.0, 255.0) as u8;
|
||||||
|
let g = (data[g_idx] * scale_factor).clamp(0.0, 255.0) as u8;
|
||||||
|
let b = (data[b_idx] * scale_factor).clamp(0.0, 255.0) as u8;
|
||||||
|
|
||||||
|
output_img_512.put_pixel(x, y, Rgba([r, g, b, 255]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resize back to original
|
||||||
|
let (orig_w, orig_h) = input_image.dimensions();
|
||||||
|
let final_inpainted = image::imageops::resize(&output_img_512, orig_w, orig_h, image::imageops::FilterType::Lanczos3);
|
||||||
|
|
||||||
|
// 5. Blending
|
||||||
|
let mut result_image = input_image.clone();
|
||||||
|
|
||||||
|
for y in 0..orig_h {
|
||||||
|
for x in 0..orig_w {
|
||||||
|
if mask_image.get_pixel(x, y)[0] > 127 {
|
||||||
|
result_image.put_pixel(x, y, *final_inpainted.get_pixel(x, y));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(result_image)
|
||||||
|
}
|
||||||
@@ -1,32 +1,94 @@
|
|||||||
use std::fs;
|
use std::fs;
|
||||||
|
use std::collections::hash_map::DefaultHasher;
|
||||||
|
use std::hash::{Hash, Hasher};
|
||||||
|
use std::env;
|
||||||
|
|
||||||
#[derive(serde::Serialize)]
|
#[derive(serde::Serialize, Clone)]
|
||||||
struct ImageItem {
|
struct ImageItem {
|
||||||
path: String,
|
path: String,
|
||||||
name: String,
|
name: String,
|
||||||
|
thumbnail: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_cache_dir() -> std::path::PathBuf {
|
||||||
|
let mut path = env::temp_dir();
|
||||||
|
path.push("watermark-wizard-thumbs");
|
||||||
|
if !path.exists() {
|
||||||
|
let _ = fs::create_dir_all(&path);
|
||||||
|
}
|
||||||
|
path
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_image_safe(path: &Path) -> Result<image::DynamicImage, String> {
|
||||||
|
let bytes = fs::read(path).map_err(|e| {
|
||||||
|
let exists = path.exists();
|
||||||
|
format!("读取文件失败 (存在: {}) / Failed to read file '{}': {}", exists, path.to_string_lossy(), e)
|
||||||
|
})?;
|
||||||
|
image::load_from_memory(&bytes).map_err(|e| format!("图片解码失败 / Failed to decode image '{}': {}", path.to_string_lossy(), e))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_thumbnail(original_path: &Path) -> Option<String> {
|
||||||
|
let cache_dir = get_cache_dir();
|
||||||
|
|
||||||
|
// Generate simple hash for filename
|
||||||
|
let mut hasher = DefaultHasher::new();
|
||||||
|
original_path.hash(&mut hasher);
|
||||||
|
let hash = hasher.finish();
|
||||||
|
let file_name = format!("{}.jpg", hash);
|
||||||
|
let thumb_path = cache_dir.join(file_name);
|
||||||
|
|
||||||
|
// Return if exists
|
||||||
|
if thumb_path.exists() {
|
||||||
|
return Some(thumb_path.to_string_lossy().to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate
|
||||||
|
if let Ok(img) = load_image_safe(original_path) {
|
||||||
|
let thumb = img.thumbnail(u32::MAX, 200);
|
||||||
|
let _file = fs::File::create(&thumb_path).ok()?;
|
||||||
|
thumb.save_with_format(&thumb_path, image::ImageFormat::Jpeg).ok()?;
|
||||||
|
|
||||||
|
return Some(thumb_path.to_string_lossy().to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn scan_dir(path: String) -> Result<Vec<ImageItem>, String> {
|
async fn scan_dir(path: String) -> Result<Vec<ImageItem>, String> {
|
||||||
let mut images = Vec::new();
|
let entries = fs::read_dir(&path).map_err(|e| e.to_string())?;
|
||||||
let dir = fs::read_dir(&path).map_err(|e| e.to_string())?;
|
|
||||||
|
|
||||||
for entry in dir {
|
// Collect valid paths first to avoid holding fs locks or iterators during parallel proc
|
||||||
|
let mut valid_paths = Vec::new();
|
||||||
|
for entry in entries {
|
||||||
if let Ok(entry) = entry {
|
if let Ok(entry) = entry {
|
||||||
let path = entry.path();
|
let p = entry.path();
|
||||||
if path.is_file() {
|
if p.is_file() {
|
||||||
if let Some(ext) = path.extension() {
|
if let Some(ext) = p.extension() {
|
||||||
let ext_str = ext.to_string_lossy().to_lowercase();
|
let ext_str = ext.to_string_lossy().to_lowercase();
|
||||||
if ["png", "jpg", "jpeg", "webp"].contains(&ext_str.as_str()) {
|
if ["png", "jpg", "jpeg", "webp"].contains(&ext_str.as_str()) {
|
||||||
images.push(ImageItem {
|
valid_paths.push(p);
|
||||||
path: path.to_string_lossy().to_string(),
|
|
||||||
name: path.file_name().unwrap_or_default().to_string_lossy().to_string(),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Process in parallel
|
||||||
|
let mut images: Vec<ImageItem> = valid_paths.par_iter().filter_map(|path| {
|
||||||
|
let name = path.file_name()?.to_string_lossy().to_string();
|
||||||
|
let path_str = path.to_string_lossy().to_string();
|
||||||
|
|
||||||
|
// Generate thumbnail
|
||||||
|
let thumb = generate_thumbnail(path).unwrap_or_else(|| path_str.clone());
|
||||||
|
|
||||||
|
Some(ImageItem {
|
||||||
|
path: path_str,
|
||||||
|
name,
|
||||||
|
thumbnail: thumb,
|
||||||
|
})
|
||||||
|
}).collect();
|
||||||
|
|
||||||
// Sort by name
|
// Sort by name
|
||||||
images.sort_by(|a, b| a.name.cmp(&b.name));
|
images.sort_by(|a, b| a.name.cmp(&b.name));
|
||||||
Ok(images)
|
Ok(images)
|
||||||
@@ -34,6 +96,18 @@ fn scan_dir(path: String) -> Result<Vec<ImageItem>, String> {
|
|||||||
|
|
||||||
use image::GenericImageView;
|
use image::GenericImageView;
|
||||||
use image::Pixel;
|
use image::Pixel;
|
||||||
|
use rayon::prelude::*;
|
||||||
|
use std::path::Path;
|
||||||
|
use imageproc::drawing::draw_text_mut;
|
||||||
|
use ab_glyph::{FontRef, PxScale};
|
||||||
|
use tauri::{AppHandle, Manager};
|
||||||
|
|
||||||
|
pub mod lama;
|
||||||
|
pub mod ocr;
|
||||||
|
pub mod ort_ops;
|
||||||
|
|
||||||
|
// Embed the font to ensure it's always available without path issues
|
||||||
|
const FONT_DATA: &[u8] = include_bytes!("../assets/fonts/Roboto-Regular.ttf");
|
||||||
|
|
||||||
#[derive(serde::Serialize)]
|
#[derive(serde::Serialize)]
|
||||||
struct ZcaResult {
|
struct ZcaResult {
|
||||||
@@ -42,28 +116,227 @@ struct ZcaResult {
|
|||||||
zone: String,
|
zone: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
struct ExportImageTask {
|
||||||
|
path: String,
|
||||||
|
output_filename: Option<String>,
|
||||||
|
manual_position: Option<ManualPosition>,
|
||||||
|
scale: Option<f64>,
|
||||||
|
opacity: Option<f64>,
|
||||||
|
color: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
struct WatermarkSettings {
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
_w_type: String, // 'text' (image is deprecated for now per user request, but keeping struct flexible)
|
||||||
|
text: String, // Was 'source'
|
||||||
|
color: String, // Hex code e.g. "#FFFFFF"
|
||||||
|
opacity: f64,
|
||||||
|
scale: f64, // Font size relative to image height (e.g., 0.05 = 5% of height)
|
||||||
|
// Global manual override is deprecated in favor of per-task control, but kept for struct compatibility if needed
|
||||||
|
_manual_override: bool,
|
||||||
|
_manual_position: ManualPosition,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
struct ManualPosition {
|
||||||
|
x: f64,
|
||||||
|
y: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_hex_color(hex: &str) -> image::Rgba<u8> {
|
||||||
|
let hex = hex.trim_start_matches('#');
|
||||||
|
let r = u8::from_str_radix(&hex[0..2], 16).unwrap_or(255);
|
||||||
|
let g = u8::from_str_radix(&hex[2..4], 16).unwrap_or(255);
|
||||||
|
let b = u8::from_str_radix(&hex[4..6], 16).unwrap_or(255);
|
||||||
|
image::Rgba([r, g, b, 255])
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn get_zca_suggestion(path: String) -> Result<ZcaResult, String> {
|
async fn export_batch(images: Vec<ExportImageTask>, watermark: WatermarkSettings, output_dir: String, mode: String) -> Result<String, String> {
|
||||||
let img = image::open(&path).map_err(|e| e.to_string())?;
|
let font = FontRef::try_from_slice(FONT_DATA).map_err(|e| format!("Font error: {}", e))?;
|
||||||
|
|
||||||
|
// Note: Settings are now resolved per-task
|
||||||
|
|
||||||
|
let results: Vec<Result<(), String>> = images.par_iter().map(|task| {
|
||||||
|
let input_path = Path::new(&task.path);
|
||||||
|
|
||||||
|
// Use safe loading helper
|
||||||
|
let img_result = load_image_safe(input_path);
|
||||||
|
|
||||||
|
if let Ok(dynamic_img) = &img_result {
|
||||||
|
let mut base_img = dynamic_img.to_rgba8();
|
||||||
|
let (width, height) = base_img.dimensions();
|
||||||
|
|
||||||
|
// ONLY EXECUTE WATERMARK LOGIC IF MODE IS 'ADD'
|
||||||
|
if mode == "add" {
|
||||||
|
// Determine effective settings (Task > Global)
|
||||||
|
let eff_scale = task.scale.unwrap_or(watermark.scale);
|
||||||
|
let eff_opacity = task.opacity.unwrap_or(watermark.opacity);
|
||||||
|
let eff_color_hex = task.color.as_ref().unwrap_or(&watermark.color);
|
||||||
|
|
||||||
|
// Calculate final color
|
||||||
|
let base_color = parse_hex_color(eff_color_hex);
|
||||||
|
let alpha = (eff_opacity * 255.0) as u8;
|
||||||
|
let text_color = image::Rgba([base_color[0], base_color[1], base_color[2], alpha]);
|
||||||
|
|
||||||
|
// 1. Calculate Font Scale based on Image Height
|
||||||
|
let mut scale_px = height as f32 * eff_scale as f32;
|
||||||
|
|
||||||
|
// 2. Measure Text
|
||||||
|
let scaled_font = PxScale::from(scale_px);
|
||||||
|
let (t_width, _t_height) = imageproc::drawing::text_size(scaled_font, &font, &watermark.text);
|
||||||
|
|
||||||
|
// 3. Ensure it fits width (Padding 15%)
|
||||||
|
let max_width = (width as f32 * 0.85) as u32;
|
||||||
|
if t_width > max_width {
|
||||||
|
let ratio = max_width as f32 / t_width as f32;
|
||||||
|
scale_px *= ratio;
|
||||||
|
}
|
||||||
|
let final_scale = PxScale::from(scale_px);
|
||||||
|
let (final_t_width, final_t_height) = imageproc::drawing::text_size(final_scale, &font, &watermark.text);
|
||||||
|
|
||||||
|
// 4. Determine Position (Task Specific > ZCA)
|
||||||
|
// If task has manual_position, use it. Otherwise calculate ZCA.
|
||||||
|
let (pos_x_pct, pos_y_pct) = if let Some(pos) = &task.manual_position {
|
||||||
|
(pos.x, pos.y)
|
||||||
|
} else {
|
||||||
|
match calculate_zca_internal(&dynamic_img) {
|
||||||
|
Ok(res) => (res.x, res.y),
|
||||||
|
Err(_) => (0.5, 0.97),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Calculate initial top-left based on center
|
||||||
|
let center_x = width as f64 * pos_x_pct;
|
||||||
|
let center_y = height as f64 * pos_y_pct;
|
||||||
|
|
||||||
|
let mut x = (center_x - (final_t_width as f64 / 2.0)) as i32;
|
||||||
|
let mut y = (center_y - (final_t_height as f64 / 2.0)) as i32;
|
||||||
|
|
||||||
|
// 5. Strict Boundary Clamping
|
||||||
|
// We ensure the text box (final_t_width, final_t_height) is always inside (0, 0, width, height)
|
||||||
|
let min_padding = 2; // Absolute minimum pixels from edge
|
||||||
|
|
||||||
|
if x < min_padding { x = min_padding; }
|
||||||
|
if y < min_padding { y = min_padding; }
|
||||||
|
if x + final_t_width as i32 > width as i32 - min_padding {
|
||||||
|
x = width as i32 - final_t_width as i32 - min_padding;
|
||||||
|
}
|
||||||
|
if y + final_t_height as i32 > height as i32 - min_padding {
|
||||||
|
y = height as i32 - final_t_height as i32 - min_padding;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-clamp just in case of very small images where text is larger than image
|
||||||
|
x = x.max(0);
|
||||||
|
y = y.max(0);
|
||||||
|
|
||||||
|
// SKIP DRAWING if text is empty (e.g. Remove Mode)
|
||||||
|
if !watermark.text.trim().is_empty() {
|
||||||
|
// 6. Draw Stroke (Simple 4-direction offset for black outline)
|
||||||
|
// Stroke alpha should match text alpha
|
||||||
|
let stroke_color = image::Rgba([0, 0, 0, text_color[3]]);
|
||||||
|
for offset in [(-1, -1), (-1, 1), (1, -1), (1, 1)] {
|
||||||
|
draw_text_mut(
|
||||||
|
&mut base_img,
|
||||||
|
stroke_color,
|
||||||
|
x + offset.0,
|
||||||
|
y + offset.1,
|
||||||
|
final_scale,
|
||||||
|
&font,
|
||||||
|
&watermark.text,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. Draw Main Text
|
||||||
|
draw_text_mut(
|
||||||
|
&mut base_img,
|
||||||
|
text_color,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
final_scale,
|
||||||
|
&font,
|
||||||
|
&watermark.text,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} // END IF MODE == ADD
|
||||||
|
|
||||||
|
// Save
|
||||||
|
// Prioritize explicitly provided output filename (from original path), fall back to input filename
|
||||||
|
let file_name = match &task.output_filename {
|
||||||
|
Some(name) => std::ffi::OsStr::new(name),
|
||||||
|
None => input_path.file_name().unwrap_or_default()
|
||||||
|
};
|
||||||
|
let output_path = Path::new(&output_dir).join(file_name);
|
||||||
|
let _output_path = Path::new(&output_dir).join(file_name);
|
||||||
|
|
||||||
|
// Handle format specific saving
|
||||||
|
// JPEG does not support Alpha channel. If we save Rgba8 to Jpeg, it might fail or look wrong.
|
||||||
|
let ext = output_path.extension().and_then(|s| s.to_str()).unwrap_or("").to_lowercase();
|
||||||
|
if ext == "jpg" || ext == "jpeg" {
|
||||||
|
// Convert to RGB8 (dropping alpha)
|
||||||
|
// Note: This simply drops alpha. If background was transparent, it becomes black.
|
||||||
|
// For photos (JPEGs) this is usually fine as they don't have alpha.
|
||||||
|
let rgb_img = image::DynamicImage::ImageRgba8(base_img).to_rgb8();
|
||||||
|
rgb_img.save(&output_path).map_err(|e| e.to_string())?;
|
||||||
|
} else {
|
||||||
|
// For PNG/WebP etc, keep RGBA
|
||||||
|
base_img.save(&output_path).map_err(|e| e.to_string())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(img_result.unwrap_err())
|
||||||
|
}
|
||||||
|
}).collect();
|
||||||
|
|
||||||
|
let failures: Vec<String> = results.into_iter().filter_map(|r| r.err()).collect();
|
||||||
|
|
||||||
|
if failures.is_empty() {
|
||||||
|
Ok("All images processed successfully".to_string())
|
||||||
|
} else {
|
||||||
|
Err(format!("Completed with errors: {:?}", failures))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to reuse logic (adapted from command)
|
||||||
|
fn calculate_zca_internal(img: &image::DynamicImage) -> Result<ZcaResult, String> {
|
||||||
let (width, height) = img.dimensions();
|
let (width, height) = img.dimensions();
|
||||||
let bottom_start_y = (height as f64 * 0.8) as u32;
|
|
||||||
let zone_height = height - bottom_start_y;
|
|
||||||
let zone_width = width / 3;
|
|
||||||
let zones = [
|
|
||||||
("Left", 0, bottom_start_y),
|
|
||||||
("Center", zone_width, bottom_start_y),
|
|
||||||
("Right", zone_width * 2, bottom_start_y),
|
|
||||||
];
|
|
||||||
|
|
||||||
let mut min_std_dev = f64::MAX;
|
// Greedy Layered Search
|
||||||
let mut best_zone = "Center";
|
// Priority: Bottom -> Up
|
||||||
let mut best_pos = (0.5, 0.9); // Default center
|
let y_levels = [0.97, 0.94, 0.91, 0.88];
|
||||||
|
let x_cols = [1.0/6.0, 3.0/6.0, 5.0/6.0]; // Left, Center, Right centers
|
||||||
|
let col_names = ["Left", "Center", "Right"];
|
||||||
|
|
||||||
for (name, start_x, start_y) in zones.iter() {
|
// Box Size for analysis (approx watermark size)
|
||||||
let mut luma_values = Vec::with_capacity((zone_width * zone_height) as usize);
|
let box_w = (width as f64 * 0.30) as u32;
|
||||||
for y in *start_y..height {
|
let box_h = (height as f64 * 0.05) as u32;
|
||||||
for x in *start_x..(*start_x + zone_width) {
|
let half_box_w = box_w / 2;
|
||||||
if x >= width { continue; }
|
let half_box_h = box_h / 2;
|
||||||
|
|
||||||
|
let mut global_best_score = f64::MAX;
|
||||||
|
let mut global_best_result = ZcaResult { x: 0.5, y: 0.97, zone: "Center".to_string() };
|
||||||
|
|
||||||
|
for &y_pct in y_levels.iter() {
|
||||||
|
let mut row_best_score = f64::MAX;
|
||||||
|
let mut row_best_idx = 1; // Default Center
|
||||||
|
let mut row_stats = Vec::new(); // (mean, std_dev)
|
||||||
|
|
||||||
|
for (col_idx, &x_pct) in x_cols.iter().enumerate() {
|
||||||
|
let cx = (width as f64 * x_pct) as u32;
|
||||||
|
let cy = (height as f64 * y_pct) as u32;
|
||||||
|
|
||||||
|
let start_x = if cx > half_box_w { cx - half_box_w } else { 0 };
|
||||||
|
let start_y = if cy > half_box_h { cy - half_box_h } else { 0 };
|
||||||
|
let end_x = (start_x + box_w).min(width);
|
||||||
|
let end_y = (start_y + box_h).min(height);
|
||||||
|
|
||||||
|
let mut luma_values = Vec::with_capacity((box_w * box_h) as usize);
|
||||||
|
|
||||||
|
for y in start_y..end_y {
|
||||||
|
for x in start_x..end_x {
|
||||||
let pixel = img.get_pixel(x, y);
|
let pixel = img.get_pixel(x, y);
|
||||||
let rgb = pixel.to_rgb();
|
let rgb = pixel.to_rgb();
|
||||||
let luma = 0.299 * rgb[0] as f64 + 0.587 * rgb[1] as f64 + 0.114 * rgb[2] as f64;
|
let luma = 0.299 * rgb[0] as f64 + 0.587 * rgb[1] as f64 + 0.114 * rgb[2] as f64;
|
||||||
@@ -72,36 +345,426 @@ fn get_zca_suggestion(path: String) -> Result<ZcaResult, String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let count = luma_values.len() as f64;
|
let count = luma_values.len() as f64;
|
||||||
if count == 0.0 { continue; }
|
if count == 0.0 {
|
||||||
|
row_stats.push((0.0, f64::MAX));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
let mean = luma_values.iter().sum::<f64>() / count;
|
let mean = luma_values.iter().sum::<f64>() / count;
|
||||||
let variance = luma_values.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / count;
|
let variance = luma_values.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / count;
|
||||||
let std_dev = variance.sqrt();
|
let std_dev = variance.sqrt();
|
||||||
|
|
||||||
if std_dev < min_std_dev {
|
row_stats.push((mean, std_dev));
|
||||||
min_std_dev = std_dev;
|
|
||||||
best_zone = name;
|
// For choosing "Best in Row", we strictly prefer Flatness (StdDev)
|
||||||
let center_x_px = *start_x as f64 + (zone_width as f64 / 2.0);
|
if std_dev < row_best_score {
|
||||||
// Position closer to bottom (75% of the zone height instead of 50%)
|
row_best_score = std_dev;
|
||||||
// Zone starts at 80%. Height is 20%. 0.8 + 0.2 * 0.75 = 0.95
|
row_best_idx = col_idx;
|
||||||
let center_y_px = *start_y as f64 + (zone_height as f64 * 0.75);
|
}
|
||||||
best_pos = (center_x_px / width as f64, center_y_px / height as f64);
|
|
||||||
|
// Update Global Best (fallback)
|
||||||
|
if std_dev < global_best_score {
|
||||||
|
global_best_score = std_dev;
|
||||||
|
global_best_result = ZcaResult {
|
||||||
|
x: x_pct,
|
||||||
|
y: y_pct,
|
||||||
|
zone: col_names[col_idx].to_string(),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(ZcaResult {
|
// Analyze the Best Zone in this Row
|
||||||
x: best_pos.0,
|
let (mean, std_dev) = row_stats[row_best_idx];
|
||||||
y: best_pos.1,
|
|
||||||
zone: best_zone.to_string(),
|
// Safety Check: Is this zone "White Text"?
|
||||||
|
// Condition: Mean > 180 (Bright-ish) AND StdDev > 20 (Busy/Text)
|
||||||
|
let is_unsafe_white_text = mean > 180.0 && std_dev > 20.0;
|
||||||
|
let is_unsafe_bright = mean > 230.0;
|
||||||
|
|
||||||
|
if !is_unsafe_white_text && !is_unsafe_bright {
|
||||||
|
// Safe!
|
||||||
|
return Ok(ZcaResult {
|
||||||
|
x: x_cols[row_best_idx],
|
||||||
|
y: y_pct,
|
||||||
|
zone: col_names[row_best_idx].to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(global_best_result)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn get_zca_suggestion(path: String) -> Result<ZcaResult, String> {
|
||||||
|
let img = load_image_safe(Path::new(&path))?;
|
||||||
|
calculate_zca_internal(&img)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
struct LayoutResult {
|
||||||
|
x: f64,
|
||||||
|
y: f64,
|
||||||
|
scale: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn layout_watermark(path: String, text: String, base_scale: f64) -> Result<LayoutResult, String> {
|
||||||
|
let img = load_image_safe(Path::new(&path))?;
|
||||||
|
let (width, height) = img.dimensions();
|
||||||
|
let font = FontRef::try_from_slice(FONT_DATA).map_err(|e| format!("Font error: {}", e))?;
|
||||||
|
|
||||||
|
// 1. Run ZCA to find best zone center (now with dark preference)
|
||||||
|
let zca = calculate_zca_internal(&img)?;
|
||||||
|
|
||||||
|
// 2. Calculate Text Dimensions at Base Scale
|
||||||
|
let mut scale_val = base_scale;
|
||||||
|
let mut scale_px = height as f32 * scale_val as f32;
|
||||||
|
let mut font_scale = PxScale::from(scale_px);
|
||||||
|
let (mut t_width, mut t_height) = imageproc::drawing::text_size(font_scale, &font, &text);
|
||||||
|
|
||||||
|
// 3. Auto-Fit Width (Limit to 85% of image width)
|
||||||
|
let max_width = (width as f32 * 0.85) as u32;
|
||||||
|
if t_width > max_width {
|
||||||
|
let ratio = max_width as f32 / t_width as f32;
|
||||||
|
scale_val *= ratio as f64;
|
||||||
|
scale_px *= ratio;
|
||||||
|
font_scale = PxScale::from(scale_px);
|
||||||
|
let dims = imageproc::drawing::text_size(font_scale, &font, &text);
|
||||||
|
t_width = dims.0;
|
||||||
|
t_height = dims.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Smart Clamping
|
||||||
|
let center_x = zca.x * width as f64;
|
||||||
|
let center_y = zca.y * height as f64;
|
||||||
|
|
||||||
|
// Add safety margin to measured text size (Renderer mismatch buffer)
|
||||||
|
let safe_t_width = t_width as f64 * 1.05;
|
||||||
|
let safe_t_height = t_height as f64 * 1.05;
|
||||||
|
|
||||||
|
let half_w = safe_t_width / 2.0;
|
||||||
|
let half_h = safe_t_height / 2.0;
|
||||||
|
|
||||||
|
// Increase edge padding to 4%
|
||||||
|
let padding = width as f64 * 0.04;
|
||||||
|
|
||||||
|
let min_x = half_w + padding;
|
||||||
|
let max_x = width as f64 - half_w - padding;
|
||||||
|
let final_x = center_x.clamp(min_x, max_x);
|
||||||
|
|
||||||
|
let min_y = half_h + padding;
|
||||||
|
let max_y = height as f64 - half_h - padding;
|
||||||
|
let final_y = center_y.clamp(min_y, max_y);
|
||||||
|
|
||||||
|
Ok(LayoutResult {
|
||||||
|
x: final_x / width as f64,
|
||||||
|
y: final_y / height as f64,
|
||||||
|
scale: scale_val,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
struct DetectionResult {
|
||||||
|
rects: Vec<Rect>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, Clone)]
|
||||||
|
struct Rect {
|
||||||
|
x: f64,
|
||||||
|
y: f64,
|
||||||
|
width: f64,
|
||||||
|
height: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn detect_watermark(app: AppHandle, path: String) -> Result<DetectionResult, String> {
|
||||||
|
let img = load_image_safe(Path::new(&path))?.to_rgba8();
|
||||||
|
|
||||||
|
// 1. Try OCR Detection
|
||||||
|
let ocr_model_path = app.path().resource_dir()
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
.join("resources")
|
||||||
|
.join("en_PP-OCRv3_det_infer.onnx");
|
||||||
|
|
||||||
|
if ocr_model_path.exists() {
|
||||||
|
println!("Using OCR model for detection");
|
||||||
|
match ocr::run_ocr_detection(&ocr_model_path, &img) {
|
||||||
|
Ok(boxes) => {
|
||||||
|
let rects = boxes.into_iter().map(|b| Rect {
|
||||||
|
x: b.x,
|
||||||
|
y: b.y,
|
||||||
|
width: b.width,
|
||||||
|
height: b.height,
|
||||||
|
}).collect();
|
||||||
|
return Ok(DetectionResult { rects });
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("OCR Detection failed: {}", e);
|
||||||
|
// Fallthrough to legacy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
eprintln!("OCR model not found at {:?}", ocr_model_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Legacy Detection (Fallback)
|
||||||
|
println!("Falling back to legacy detection");
|
||||||
|
let (width, height) = img.dimensions();
|
||||||
|
let gray = image::DynamicImage::ImageRgba8(img.clone()).to_luma8();
|
||||||
|
|
||||||
|
let cell_size = 10;
|
||||||
|
let grid_w = (width + cell_size - 1) / cell_size;
|
||||||
|
let grid_h = (height + cell_size - 1) / cell_size;
|
||||||
|
let mut grid = vec![false; (grid_w * grid_h) as usize];
|
||||||
|
|
||||||
|
let top_limit = (height as f64 * 0.15) as u32;
|
||||||
|
let bottom_start = (height as f64 * 0.75) as u32;
|
||||||
|
let max_stroke_width = 15;
|
||||||
|
let contrast_threshold = 40;
|
||||||
|
let brightness_threshold = 200;
|
||||||
|
|
||||||
|
for y in 1..height-1 {
|
||||||
|
if y > top_limit && y < bottom_start { continue; }
|
||||||
|
|
||||||
|
for x in 1..width-1 {
|
||||||
|
let p = gray.get_pixel(x, y)[0];
|
||||||
|
if p < brightness_threshold { continue; }
|
||||||
|
|
||||||
|
let mut is_stroke = false;
|
||||||
|
let mut left_bound = false;
|
||||||
|
let mut right_bound = false;
|
||||||
|
|
||||||
|
for k in 1..=max_stroke_width {
|
||||||
|
if x < k { break; }
|
||||||
|
let neighbor = gray.get_pixel(x - k, y)[0];
|
||||||
|
if p > neighbor && (p - neighbor) > contrast_threshold {
|
||||||
|
left_bound = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if left_bound {
|
||||||
|
for k in 1..=max_stroke_width {
|
||||||
|
if x + k >= width { break; }
|
||||||
|
let neighbor = gray.get_pixel(x + k, y)[0];
|
||||||
|
if p > neighbor && (p - neighbor) > contrast_threshold {
|
||||||
|
right_bound = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if left_bound && right_bound {
|
||||||
|
is_stroke = true;
|
||||||
|
} else {
|
||||||
|
let mut up_bound = false;
|
||||||
|
let mut down_bound = false;
|
||||||
|
for k in 1..=max_stroke_width {
|
||||||
|
if y < k { break; }
|
||||||
|
let neighbor = gray.get_pixel(x, y - k)[0];
|
||||||
|
if p > neighbor && (p - neighbor) > contrast_threshold {
|
||||||
|
up_bound = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if up_bound {
|
||||||
|
for k in 1..=max_stroke_width {
|
||||||
|
if y + k >= height { break; }
|
||||||
|
let neighbor = gray.get_pixel(x, y + k)[0];
|
||||||
|
if p > neighbor && (p - neighbor) > contrast_threshold {
|
||||||
|
down_bound = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if up_bound && down_bound { is_stroke = true; }
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_stroke {
|
||||||
|
let gx = x / cell_size;
|
||||||
|
let gy = y / cell_size;
|
||||||
|
grid[(gy * grid_w + gx) as usize] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut rects = Vec::new();
|
||||||
|
let mut visited = vec![false; grid.len()];
|
||||||
|
|
||||||
|
for gy in 0..grid_h {
|
||||||
|
for gx in 0..grid_w {
|
||||||
|
let idx = (gy * grid_w + gx) as usize;
|
||||||
|
if grid[idx] && !visited[idx] {
|
||||||
|
let mut min_gx = gx;
|
||||||
|
let mut max_gx = gx;
|
||||||
|
let mut min_gy = gy;
|
||||||
|
let mut max_gy = gy;
|
||||||
|
|
||||||
|
let mut stack = vec![(gx, gy)];
|
||||||
|
visited[idx] = true;
|
||||||
|
|
||||||
|
while let Some((cx, cy)) = stack.pop() {
|
||||||
|
if cx < min_gx { min_gx = cx; }
|
||||||
|
if cx > max_gx { max_gx = cx; }
|
||||||
|
if cy < min_gy { min_gy = cy; }
|
||||||
|
if cy > max_gy { max_gy = cy; }
|
||||||
|
|
||||||
|
let neighbors = [
|
||||||
|
(cx.wrapping_sub(1), cy), (cx + 1, cy),
|
||||||
|
(cx, cy.wrapping_sub(1)), (cx, cy + 1)
|
||||||
|
];
|
||||||
|
|
||||||
|
for (nx, ny) in neighbors {
|
||||||
|
if nx < grid_w && ny < grid_h {
|
||||||
|
let nidx = (ny * grid_w + nx) as usize;
|
||||||
|
if grid[nidx] && !visited[nidx] {
|
||||||
|
visited[nidx] = true;
|
||||||
|
stack.push((nx, ny));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let px = (min_gx * cell_size) as f64;
|
||||||
|
let py = (min_gy * cell_size) as f64;
|
||||||
|
let pw = ((max_gx - min_gx + 1) * cell_size) as f64;
|
||||||
|
let ph = ((max_gy - min_gy + 1) * cell_size) as f64;
|
||||||
|
|
||||||
|
rects.push(Rect {
|
||||||
|
x: px / width as f64,
|
||||||
|
y: py / height as f64,
|
||||||
|
width: pw / width as f64,
|
||||||
|
height: ph / height as f64,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(DetectionResult { rects })
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
struct StrokePoint {
|
||||||
|
x: f64,
|
||||||
|
y: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
struct StrokeRect {
|
||||||
|
x: f64,
|
||||||
|
y: f64,
|
||||||
|
w: f64,
|
||||||
|
h: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
#[serde(tag = "type")]
|
||||||
|
enum MaskStroke {
|
||||||
|
#[serde(rename = "path")]
|
||||||
|
Path {
|
||||||
|
points: Vec<StrokePoint>,
|
||||||
|
width: f64,
|
||||||
|
},
|
||||||
|
#[serde(rename = "rect")]
|
||||||
|
Rect {
|
||||||
|
rect: StrokeRect,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn run_inpainting(app: AppHandle, path: String, strokes: Vec<MaskStroke>) -> Result<String, String> {
|
||||||
|
let img = load_image_safe(Path::new(&path))?.to_rgba8();
|
||||||
|
let (width, height) = img.dimensions();
|
||||||
|
|
||||||
|
// 1. Create Gray Mask (0 = keep, 255 = remove)
|
||||||
|
let mut mask = image::GrayImage::new(width, height);
|
||||||
|
|
||||||
|
for stroke in strokes {
|
||||||
|
match stroke {
|
||||||
|
MaskStroke::Rect { rect } => {
|
||||||
|
let x1 = (rect.x * width as f64) as i32;
|
||||||
|
let y1 = (rect.y * height as f64) as i32;
|
||||||
|
let w = (rect.w * width as f64) as i32;
|
||||||
|
let h = (rect.h * height as f64) as i32;
|
||||||
|
|
||||||
|
// Draw 255 on mask
|
||||||
|
for y in y1..(y1 + h) {
|
||||||
|
for x in x1..(x1 + w) {
|
||||||
|
if x >= 0 && x < width as i32 && y >= 0 && y < height as i32 {
|
||||||
|
mask.put_pixel(x as u32, y as u32, image::Luma([255]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
MaskStroke::Path { points, width: stroke_w_pct } => {
|
||||||
|
if points.is_empty() { continue; }
|
||||||
|
let r = (stroke_w_pct * width as f64 / 2.0).ceil() as i32;
|
||||||
|
let r2 = r * r;
|
||||||
|
|
||||||
|
for i in 0..points.len() - 1 {
|
||||||
|
let p1 = &points[i];
|
||||||
|
let p2 = &points[i+1];
|
||||||
|
|
||||||
|
let x1 = p1.x * width as f64;
|
||||||
|
let y1 = p1.y * height as f64;
|
||||||
|
let x2 = p2.x * width as f64;
|
||||||
|
let y2 = p2.y * height as f64;
|
||||||
|
|
||||||
|
// Simple line interpolation
|
||||||
|
let dist = ((x2-x1).powi(2) + (y2-y1).powi(2)).sqrt();
|
||||||
|
let steps = dist.max(1.0) as i32;
|
||||||
|
|
||||||
|
for s in 0..=steps {
|
||||||
|
let t = s as f64 / steps as f64;
|
||||||
|
let cx = (x1 + (x2 - x1) * t) as i32;
|
||||||
|
let cy = (y1 + (y2 - y1) * t) as i32;
|
||||||
|
|
||||||
|
// Draw circle at cx, cy
|
||||||
|
for dy in -r..=r {
|
||||||
|
for dx in -r..=r {
|
||||||
|
if dx*dx + dy*dy <= r2 {
|
||||||
|
let nx = cx + dx;
|
||||||
|
let ny = cy + dy;
|
||||||
|
if nx >= 0 && nx < width as i32 && ny >= 0 && ny < height as i32 {
|
||||||
|
mask.put_pixel(nx as u32, ny as u32, image::Luma([255]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Resolve Model Path
|
||||||
|
let model_path = app.path().resource_dir()
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
.join("resources")
|
||||||
|
.join("lama_fp32.onnx");
|
||||||
|
|
||||||
|
if !model_path.exists() {
|
||||||
|
return Err("Model file 'lama_fp32.onnx' not found in resources.".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Run Inference
|
||||||
|
// This is computationally heavy, maybe run in thread? Tauri async commands are already threaded.
|
||||||
|
let result_img = lama::run_lama_inpainting(&model_path, &img, &mask)?;
|
||||||
|
|
||||||
|
// Save to temp
|
||||||
|
let cache_dir = get_cache_dir();
|
||||||
|
let file_name = format!("inpainted_{}.png", std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_millis());
|
||||||
|
let out_path = cache_dir.join(file_name);
|
||||||
|
|
||||||
|
result_img.save(&out_path).map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
Ok(out_path.to_string_lossy().to_string())
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_dialog::init())
|
.plugin(tauri_plugin_dialog::init())
|
||||||
.invoke_handler(tauri::generate_handler![scan_dir, get_zca_suggestion])
|
.invoke_handler(tauri::generate_handler![scan_dir, get_zca_suggestion, export_batch, detect_watermark, layout_watermark, run_inpainting])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
}
|
}
|
||||||
|
|||||||
202
src-tauri/src/ocr.rs
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
use crate::ort_ops;
|
||||||
|
use image::{GenericImageView, Rgba, RgbaImage};
|
||||||
|
use ort::value::Value;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct DetectedBox {
|
||||||
|
pub x: f64,
|
||||||
|
pub y: f64,
|
||||||
|
pub width: f64,
|
||||||
|
pub height: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run_ocr_detection(
|
||||||
|
model_path: &Path,
|
||||||
|
input_image: &RgbaImage,
|
||||||
|
) -> Result<Vec<DetectedBox>, String> {
|
||||||
|
// 1. Load Model using the shared function
|
||||||
|
let mut session = ort_ops::create_session(model_path)
|
||||||
|
.map_err(|e| format!("Failed to create ORT session: {}", e))?;
|
||||||
|
|
||||||
|
// 2. Preprocess
|
||||||
|
// DBNet expects standard normalization: (img - mean) / std
|
||||||
|
// Mean: [0.485, 0.456, 0.406], Std: [0.229, 0.224, 0.225]
|
||||||
|
// And usually resized to multiple of 32. Limit max size for speed.
|
||||||
|
let max_side = 1600; // Increase resolution limit
|
||||||
|
let (orig_w, orig_h) = input_image.dimensions();
|
||||||
|
|
||||||
|
// --- CROP to top 5% and bottom 5% ---
|
||||||
|
let crop_height = (orig_h as f64 * 0.05).ceil() as u32;
|
||||||
|
let mut masked_image = RgbaImage::from_pixel(orig_w, orig_h, Rgba([0, 0, 0, 255]));
|
||||||
|
|
||||||
|
if crop_height > 0 {
|
||||||
|
// Copy top part
|
||||||
|
let top_view = input_image.view(0, 0, orig_w, crop_height).to_image();
|
||||||
|
image::imageops::replace(&mut masked_image, &top_view, 0, 0);
|
||||||
|
|
||||||
|
// Copy bottom part
|
||||||
|
let bottom_y = orig_h.saturating_sub(crop_height);
|
||||||
|
let bottom_view = input_image.view(0, bottom_y, orig_w, crop_height).to_image();
|
||||||
|
image::imageops::replace(&mut masked_image, &bottom_view, 0, bottom_y as i64);
|
||||||
|
} else {
|
||||||
|
// If image is very short, just use the original
|
||||||
|
masked_image = input_image.clone();
|
||||||
|
}
|
||||||
|
// --- End CROP ---
|
||||||
|
|
||||||
|
let mut resize_w = orig_w;
|
||||||
|
let mut resize_h = orig_h;
|
||||||
|
|
||||||
|
// Resize logic: Limit max side, preserve aspect, ensure divisible by 32
|
||||||
|
if resize_w > max_side || resize_h > max_side {
|
||||||
|
let ratio = max_side as f64 / (orig_w.max(orig_h) as f64);
|
||||||
|
resize_w = (orig_w as f64 * ratio) as u32;
|
||||||
|
resize_h = (orig_h as f64 * ratio) as u32;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Align to 32
|
||||||
|
resize_w = (resize_w + 31) / 32 * 32;
|
||||||
|
resize_h = (resize_h + 31) / 32 * 32;
|
||||||
|
|
||||||
|
// Minimum size
|
||||||
|
resize_w = resize_w.max(32);
|
||||||
|
resize_h = resize_h.max(32);
|
||||||
|
|
||||||
|
let resized = image::imageops::resize(&masked_image, resize_w, resize_h, image::imageops::FilterType::Triangle);
|
||||||
|
|
||||||
|
let channel_stride = (resize_w * resize_h) as usize;
|
||||||
|
let mut input_data = Vec::with_capacity(1 * 3 * channel_stride);
|
||||||
|
let mut r_plane = Vec::with_capacity(channel_stride);
|
||||||
|
let mut g_plane = Vec::with_capacity(channel_stride);
|
||||||
|
let mut b_plane = Vec::with_capacity(channel_stride);
|
||||||
|
|
||||||
|
let mean = [0.485, 0.456, 0.406];
|
||||||
|
let std = [0.229, 0.224, 0.225];
|
||||||
|
|
||||||
|
for (_x, _y, pixel) in resized.enumerate_pixels() {
|
||||||
|
let r = pixel[0] as f32 / 255.0;
|
||||||
|
let g = pixel[1] as f32 / 255.0;
|
||||||
|
let b = pixel[2] as f32 / 255.0;
|
||||||
|
|
||||||
|
r_plane.push((r - mean[0]) / std[0]);
|
||||||
|
g_plane.push((g - mean[1]) / std[1]);
|
||||||
|
b_plane.push((b - mean[2]) / std[2]);
|
||||||
|
}
|
||||||
|
input_data.extend(r_plane);
|
||||||
|
input_data.extend(g_plane);
|
||||||
|
input_data.extend(b_plane);
|
||||||
|
|
||||||
|
// 3. Inference
|
||||||
|
let input_shape = vec![1, 3, resize_h as i64, resize_w as i64];
|
||||||
|
let input_value = Value::from_array((input_shape, input_data))
|
||||||
|
.map_err(|e| format!("Failed to create input tensor: {}", e))?;
|
||||||
|
|
||||||
|
let inputs = ort::inputs![input_value]; // For PP-OCR, usually just one input "x"
|
||||||
|
let outputs = session.run(inputs).map_err(|e| format!("Inference failed: {}", e))?;
|
||||||
|
|
||||||
|
let output_tensor = outputs.values().next().ok_or("No output")?;
|
||||||
|
let (shape, data) = output_tensor.try_extract_tensor::<f32>()
|
||||||
|
.map_err(|e| format!("Failed to extract output: {}", e))?;
|
||||||
|
|
||||||
|
// 4. Post-process
|
||||||
|
// Output shape is [1, 1, H, W] probability map
|
||||||
|
if shape.len() < 4 {
|
||||||
|
return Err("Unexpected output shape".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let map_w = shape[3] as u32;
|
||||||
|
let map_h = shape[2] as u32;
|
||||||
|
|
||||||
|
// Create binary map (threshold 0.3)
|
||||||
|
let threshold = 0.3;
|
||||||
|
let mut binary_map = vec![false; (map_w * map_h) as usize];
|
||||||
|
|
||||||
|
for i in 0..binary_map.len() {
|
||||||
|
if data[i] > threshold {
|
||||||
|
binary_map[i] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find Connected Components (Simple Bounding Box finding)
|
||||||
|
let mut visited = vec![false; binary_map.len()];
|
||||||
|
let mut boxes = Vec::new();
|
||||||
|
|
||||||
|
for y in 0..map_h {
|
||||||
|
for x in 0..map_w {
|
||||||
|
let idx = (y * map_w + x) as usize;
|
||||||
|
if binary_map[idx] && !visited[idx] {
|
||||||
|
// Flood fill
|
||||||
|
let mut stack = vec![(x as u32, y as u32)];
|
||||||
|
visited[idx] = true;
|
||||||
|
|
||||||
|
let mut min_x = x;
|
||||||
|
let mut max_x = x;
|
||||||
|
let mut min_y = y;
|
||||||
|
let mut max_y = y;
|
||||||
|
let mut pixel_count = 0;
|
||||||
|
|
||||||
|
while let Some((cx, cy)) = stack.pop() {
|
||||||
|
pixel_count += 1;
|
||||||
|
if cx < min_x { min_x = cx; }
|
||||||
|
if cx > max_x { max_x = cx; }
|
||||||
|
if cy < min_y { min_y = cy; }
|
||||||
|
if cy > max_y { max_y = cy; }
|
||||||
|
|
||||||
|
let neighbors = [
|
||||||
|
(cx.wrapping_sub(1), cy), (cx + 1, cy),
|
||||||
|
(cx, cy.wrapping_sub(1)), (cx, cy + 1)
|
||||||
|
];
|
||||||
|
|
||||||
|
for (nx, ny) in neighbors {
|
||||||
|
if nx < map_w && ny < map_h {
|
||||||
|
let nidx = (ny * map_w + nx) as usize;
|
||||||
|
if binary_map[nidx] && !visited[nidx] {
|
||||||
|
visited[nidx] = true;
|
||||||
|
stack.push((nx, ny));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter small noise
|
||||||
|
if pixel_count < 10 { continue; }
|
||||||
|
|
||||||
|
// Calculate Scale Factors
|
||||||
|
let scale_x = orig_w as f64 / resize_w as f64;
|
||||||
|
let scale_y = orig_h as f64 / resize_h as f64;
|
||||||
|
|
||||||
|
// Map to raw coordinates in map space
|
||||||
|
let raw_w = (max_x - min_x + 1) as f64;
|
||||||
|
let raw_h = (max_y - min_y + 1) as f64;
|
||||||
|
|
||||||
|
// --- ASPECT RATIO FILTERING ---
|
||||||
|
// Watermarks are typically horizontal text lines.
|
||||||
|
// A cross or vertical pillar will have a small width/height ratio.
|
||||||
|
let aspect_ratio = raw_w / raw_h;
|
||||||
|
if aspect_ratio < 1.5 {
|
||||||
|
continue; // Skip vertical or square-ish non-text objects
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- PADDING / DILATION ---
|
||||||
|
let pad_x = raw_w * 0.15; // 15% horizontal is usually enough
|
||||||
|
let pad_y = raw_h * 1.00; // Increased to 100% for aggressive vertical coverage
|
||||||
|
|
||||||
|
let box_x = (min_x as f64 - pad_x).max(0.0);
|
||||||
|
let box_y = (min_y as f64 - pad_y).max(0.0);
|
||||||
|
let box_w = raw_w + 2.0 * pad_x;
|
||||||
|
let box_h = raw_h + 2.0 * pad_y;
|
||||||
|
|
||||||
|
// Convert to Normalized Image Coordinates [0, 1]
|
||||||
|
boxes.push(DetectedBox {
|
||||||
|
x: (box_x * scale_x) / orig_w as f64,
|
||||||
|
y: (box_y * scale_y) / orig_h as f64,
|
||||||
|
width: (box_w * scale_x) / orig_w as f64,
|
||||||
|
height: (box_h * scale_y) / orig_h as f64,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(boxes)
|
||||||
|
}
|
||||||
41
src-tauri/src/ort_ops.rs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
use ort::session::{Session, builder::GraphOptimizationLevel};
|
||||||
|
use ort::execution_providers::DirectMLExecutionProvider;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
/// Attempts to create an ORT session with GPU (DirectML) acceleration.
|
||||||
|
/// If GPU initialization fails, it falls back to a CPU-only session.
|
||||||
|
pub fn create_session(model_path: &Path) -> Result<Session, ort::Error> {
|
||||||
|
|
||||||
|
// Try to build with DirectML
|
||||||
|
let dm_provider = DirectMLExecutionProvider::default().build();
|
||||||
|
let session_builder = Session::builder()?
|
||||||
|
.with_optimization_level(GraphOptimizationLevel::Level3)?
|
||||||
|
.with_intra_threads(4)?;
|
||||||
|
|
||||||
|
match session_builder.with_execution_providers([dm_provider]) {
|
||||||
|
Ok(builder_with_dm) => {
|
||||||
|
println!("Attempting to commit session with DirectML provider...");
|
||||||
|
match builder_with_dm.commit_from_file(model_path) {
|
||||||
|
Ok(session) => {
|
||||||
|
println!("Successfully created ORT session with DirectML GPU acceleration.");
|
||||||
|
return Ok(session);
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
println!("Failed to create session with DirectML: {:?}. Falling back to CPU.", e);
|
||||||
|
// Fall through to CPU execution
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
println!("Failed to build session with DirectML provider: {:?}. Falling back to CPU.", e);
|
||||||
|
// Fall through to CPU execution
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fallback to CPU
|
||||||
|
println!("Creating ORT session with CPU provider.");
|
||||||
|
Session::builder()?
|
||||||
|
.with_optimization_level(GraphOptimizationLevel::Level3)?
|
||||||
|
.with_intra_threads(4)?
|
||||||
|
.commit_from_file(model_path)
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "watermark-wizard",
|
"productName": "watermark-wizard",
|
||||||
"version": "0.1.0",
|
"version": "1.0.1",
|
||||||
"identifier": "top.volan.watermark-wizard",
|
"identifier": "top.volan.watermark-wizard",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "pnpm dev",
|
"beforeDevCommand": "pnpm dev",
|
||||||
@@ -12,13 +12,13 @@
|
|||||||
"app": {
|
"app": {
|
||||||
"windows": [
|
"windows": [
|
||||||
{
|
{
|
||||||
"title": "watermark-wizard",
|
"title": "水印精灵 v1.0.1",
|
||||||
"width": 800,
|
"width": 1650,
|
||||||
"height": 600
|
"height": 1000
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"security": {
|
"security": {
|
||||||
"csp": "default-src 'self'; img-src 'self' asset: https://asset.localhost",
|
"csp": "default-src 'self'; img-src 'self' asset: http://asset.localhost https://asset.localhost blob: data:",
|
||||||
"assetProtocol": {
|
"assetProtocol": {
|
||||||
"enable": true,
|
"enable": true,
|
||||||
"scope": ["**"]
|
"scope": ["**"]
|
||||||
@@ -28,6 +28,7 @@
|
|||||||
"bundle": {
|
"bundle": {
|
||||||
"active": true,
|
"active": true,
|
||||||
"targets": "all",
|
"targets": "all",
|
||||||
|
"resources": ["resources/lama_fp32.onnx", "resources/en_PP-OCRv3_det_infer.onnx"],
|
||||||
"icon": [
|
"icon": [
|
||||||
"icons/32x32.png",
|
"icons/32x32.png",
|
||||||
"icons/128x128.png",
|
"icons/128x128.png",
|
||||||
|
|||||||
118
src/App.vue
@@ -1,11 +1,31 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import HeroView from "./components/HeroView.vue";
|
import HeroView from "./components/HeroView.vue";
|
||||||
import ThumbnailStrip from "./components/ThumbnailStrip.vue";
|
import ThumbnailStrip from "./components/ThumbnailStrip.vue";
|
||||||
|
import SettingsPanel from "./components/SettingsPanel.vue";
|
||||||
import { useGalleryStore } from "./stores/gallery";
|
import { useGalleryStore } from "./stores/gallery";
|
||||||
import { open } from '@tauri-apps/plugin-dialog';
|
import { open } from '@tauri-apps/plugin-dialog';
|
||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
|
import { FolderOpen, Download } from 'lucide-vue-next';
|
||||||
|
import { ref, onMounted, onUnmounted } from "vue";
|
||||||
|
|
||||||
const store = useGalleryStore();
|
const store = useGalleryStore();
|
||||||
|
const isExporting = ref(false);
|
||||||
|
|
||||||
|
function handleKeydown(event: KeyboardEvent) {
|
||||||
|
if (event.key === 'ArrowRight') {
|
||||||
|
store.nextImage();
|
||||||
|
} else if (event.key === 'ArrowLeft') {
|
||||||
|
store.prevImage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener('keydown', handleKeydown);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('keydown', handleKeydown);
|
||||||
|
});
|
||||||
|
|
||||||
async function openFolder() {
|
async function openFolder() {
|
||||||
try {
|
try {
|
||||||
@@ -25,26 +45,108 @@ async function openFolder() {
|
|||||||
console.error("Failed to open folder:", e);
|
console.error("Failed to open folder:", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function exportBatch() {
|
||||||
|
if (store.images.length === 0) return;
|
||||||
|
|
||||||
|
// Only require text if in ADD mode
|
||||||
|
if (store.editMode === 'add' && !store.watermarkSettings.text) {
|
||||||
|
alert("请输入水印文字。");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const outputDir = await open({
|
||||||
|
directory: true,
|
||||||
|
multiple: false,
|
||||||
|
title: "选择输出目录"
|
||||||
|
});
|
||||||
|
|
||||||
|
if (outputDir && typeof outputDir === 'string') {
|
||||||
|
isExporting.value = true;
|
||||||
|
|
||||||
|
// Map images to include manual settings
|
||||||
|
const exportTasks = store.images.map(img => {
|
||||||
|
// Extract filename from originalPath to ensure export uses original name
|
||||||
|
// Handles both Windows (\) and Unix (/) separators
|
||||||
|
const originalName = img.originalPath.split(/[/\\]/).pop() || "image.png";
|
||||||
|
|
||||||
|
return {
|
||||||
|
path: img.path,
|
||||||
|
output_filename: originalName,
|
||||||
|
manual_position: img.manualPosition || null,
|
||||||
|
scale: img.scale || null,
|
||||||
|
opacity: img.opacity || null,
|
||||||
|
color: img.color || null
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Pass dummy globals for rust struct compatibility
|
||||||
|
// The backend struct fields are named _manual_override and _manual_position
|
||||||
|
const rustWatermarkSettings = {
|
||||||
|
...store.watermarkSettings,
|
||||||
|
_manual_override: false,
|
||||||
|
_manual_position: { x: 0.5, y: 0.5 }
|
||||||
|
};
|
||||||
|
|
||||||
|
await invoke('export_batch', {
|
||||||
|
images: exportTasks,
|
||||||
|
watermark: rustWatermarkSettings,
|
||||||
|
outputDir: outputDir,
|
||||||
|
mode: store.editMode
|
||||||
|
});
|
||||||
|
alert("批量导出完成!");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Export failed:", e);
|
||||||
|
alert("导出失败: " + e);
|
||||||
|
} finally {
|
||||||
|
isExporting.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="h-screen w-screen bg-gray-900 text-white overflow-hidden flex flex-col">
|
<div class="h-screen w-screen bg-gray-900 text-white overflow-hidden flex flex-col">
|
||||||
<header class="h-12 bg-gray-800 flex items-center justify-between px-4 border-b border-gray-700 shrink-0">
|
<header class="h-14 bg-gray-800 flex items-center justify-between px-6 border-b border-gray-700 shrink-0 shadow-md z-10">
|
||||||
<h1 class="text-sm font-bold tracking-wider">WATERMARK WIZARD</h1>
|
<div class="flex items-center gap-4">
|
||||||
|
<h1 class="text-lg font-bold tracking-wider bg-linear-to-r from-blue-400 to-purple-500 bg-clip-text text-transparent">水印精灵</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
<button
|
<button
|
||||||
@click="openFolder"
|
@click="openFolder"
|
||||||
class="bg-blue-600 hover:bg-blue-500 text-white px-3 py-1 rounded text-sm transition-colors"
|
class="flex items-center gap-2 bg-gray-700 hover:bg-gray-600 text-gray-200 px-4 py-2 rounded-md text-sm font-medium transition-all hover:shadow-lg"
|
||||||
>
|
>
|
||||||
Open Folder
|
<FolderOpen class="w-4 h-4" />
|
||||||
|
打开文件夹
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="exportBatch"
|
||||||
|
:disabled="isExporting"
|
||||||
|
class="flex items-center gap-2 bg-blue-600 hover:bg-blue-500 disabled:bg-blue-800 disabled:cursor-not-allowed text-white px-4 py-2 rounded-md text-sm font-medium transition-all hover:shadow-lg shadow-blue-900/20"
|
||||||
|
>
|
||||||
|
<Download class="w-4 h-4" v-if="!isExporting" />
|
||||||
|
<div v-else class="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin"></div>
|
||||||
|
{{ isExporting ? '导出中...' : '批量导出' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main class="flex-1 relative bg-black overflow-hidden">
|
<div class="flex-1 flex overflow-hidden">
|
||||||
|
<main class="flex-1 relative bg-black flex flex-col min-w-0">
|
||||||
|
<div class="flex-1 relative overflow-hidden">
|
||||||
<HeroView />
|
<HeroView />
|
||||||
</main>
|
</div>
|
||||||
|
<footer class="h-32 bg-gray-800 border-t border-gray-700 shrink-0 z-10">
|
||||||
<footer class="h-32 bg-gray-800 border-t border-gray-700 shrink-0">
|
|
||||||
<ThumbnailStrip />
|
<ThumbnailStrip />
|
||||||
</footer>
|
</footer>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<aside class="w-80 shrink-0 h-full border-l border-gray-700">
|
||||||
|
<SettingsPanel />
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,40 +1,322 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useGalleryStore } from "../stores/gallery";
|
import { useGalleryStore } from "../stores/gallery";
|
||||||
import { convertFileSrc } from "@tauri-apps/api/core";
|
import { convertFileSrc } from "@tauri-apps/api/core";
|
||||||
|
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from "vue";
|
||||||
|
|
||||||
const store = useGalleryStore();
|
const store = useGalleryStore();
|
||||||
|
const isDragging = ref(false);
|
||||||
|
const dragStart = ref({ x: 0, y: 0 });
|
||||||
|
const imgRef = ref<HTMLImageElement | null>(null);
|
||||||
|
const parentRef = ref<HTMLElement | null>(null); // The black background container
|
||||||
|
|
||||||
|
// These dimensions will exactly match the rendered image size
|
||||||
|
const imageRect = ref({ width: 0, height: 0 });
|
||||||
|
|
||||||
|
let resizeObserver: ResizeObserver | null = null;
|
||||||
|
|
||||||
|
const calculateLayout = () => {
|
||||||
|
if (!imgRef.value || !parentRef.value || !store.selectedImage) return;
|
||||||
|
|
||||||
|
// Wait for image natural dimensions to be available
|
||||||
|
const natW = imgRef.value.naturalWidth;
|
||||||
|
const natH = imgRef.value.naturalHeight;
|
||||||
|
|
||||||
|
if (!natW || !natH) return; // Not loaded yet
|
||||||
|
|
||||||
|
const parentW = parentRef.value.clientWidth;
|
||||||
|
const parentH = parentRef.value.clientHeight;
|
||||||
|
|
||||||
|
// Calculate 'contain' fit manually
|
||||||
|
const scale = Math.min(
|
||||||
|
(parentW - 64) / natW, // 64px = 2rem padding * 2 sides (p-8)
|
||||||
|
(parentH - 64) / natH
|
||||||
|
);
|
||||||
|
|
||||||
|
const finalW = Math.floor(natW * scale);
|
||||||
|
const finalH = Math.floor(natH * scale);
|
||||||
|
|
||||||
|
imageRect.value = { width: finalW, height: finalH };
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// Observe the parent container (window size changes)
|
||||||
|
if (parentRef.value) {
|
||||||
|
resizeObserver = new ResizeObserver(() => {
|
||||||
|
calculateLayout();
|
||||||
|
});
|
||||||
|
resizeObserver.observe(parentRef.value);
|
||||||
|
}
|
||||||
|
window.addEventListener('resize', calculateLayout);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (resizeObserver) resizeObserver.disconnect();
|
||||||
|
window.removeEventListener('resize', calculateLayout);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Re-calculate when image changes
|
||||||
|
watch(() => store.selectedImage, () => {
|
||||||
|
// Reset size until loaded to avoid jump
|
||||||
|
// imageRect.value = { width: 0, height: 0 };
|
||||||
|
nextTick(() => calculateLayout());
|
||||||
|
});
|
||||||
|
|
||||||
|
// Use either manual position (if override is true) or ZCA suggestion
|
||||||
|
const position = computed(() => {
|
||||||
|
if (store.selectedImage?.manualPosition) {
|
||||||
|
return store.selectedImage.manualPosition;
|
||||||
|
}
|
||||||
|
// Default to bottom center if no ZCA
|
||||||
|
return store.selectedImage?.zcaSuggestion ? { x: store.selectedImage.zcaSuggestion.x, y: store.selectedImage.zcaSuggestion.y } : { x: 0.5, y: 0.97 };
|
||||||
|
});
|
||||||
|
|
||||||
|
const effectiveScale = computed(() => store.selectedImage?.scale ?? store.watermarkSettings.scale);
|
||||||
|
const effectiveOpacity = computed(() => store.selectedImage?.opacity ?? store.watermarkSettings.opacity);
|
||||||
|
const effectiveColor = computed(() => store.selectedImage?.color ?? store.watermarkSettings.color);
|
||||||
|
|
||||||
|
const canvasRef = ref<HTMLCanvasElement | null>(null);
|
||||||
|
const isDrawing = ref(false);
|
||||||
|
const currentPath = ref<{x: number, y: number}[]>([]);
|
||||||
|
|
||||||
|
// Redraw when strokes change or layout changes
|
||||||
|
watch(
|
||||||
|
() => store.selectedImage?.maskStrokes,
|
||||||
|
() => {
|
||||||
|
nextTick(redrawCanvas);
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(imageRect, () => {
|
||||||
|
nextTick(redrawCanvas);
|
||||||
|
});
|
||||||
|
|
||||||
|
const redrawCanvas = () => {
|
||||||
|
if (!canvasRef.value || !store.selectedImage || imageRect.value.width === 0) return;
|
||||||
|
const ctx = canvasRef.value.getContext('2d');
|
||||||
|
if (!ctx) return;
|
||||||
|
|
||||||
|
const w = imageRect.value.width;
|
||||||
|
const h = imageRect.value.height;
|
||||||
|
|
||||||
|
ctx.clearRect(0, 0, w, h);
|
||||||
|
|
||||||
|
if (!store.selectedImage.maskStrokes) return;
|
||||||
|
|
||||||
|
ctx.fillStyle = 'rgba(255, 0, 0, 0.5)';
|
||||||
|
ctx.strokeStyle = 'rgba(255, 0, 0, 0.5)';
|
||||||
|
ctx.lineCap = 'round';
|
||||||
|
ctx.lineJoin = 'round';
|
||||||
|
|
||||||
|
store.selectedImage.maskStrokes.forEach(stroke => {
|
||||||
|
if (stroke.type === 'rect' && stroke.rect) {
|
||||||
|
ctx.fillRect(
|
||||||
|
stroke.rect.x * w,
|
||||||
|
stroke.rect.y * h,
|
||||||
|
stroke.rect.w * w,
|
||||||
|
stroke.rect.h * h
|
||||||
|
);
|
||||||
|
} else if (stroke.type === 'path' && stroke.points) {
|
||||||
|
const lw = (stroke.width || (store.brushSettings.size / w)) * w;
|
||||||
|
ctx.lineWidth = lw;
|
||||||
|
|
||||||
|
if (stroke.points.length > 0) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(stroke.points[0].x * w, stroke.points[0].y * h);
|
||||||
|
for (let i = 1; i < stroke.points.length; i++) {
|
||||||
|
ctx.lineTo(stroke.points[i].x * w, stroke.points[i].y * h);
|
||||||
|
}
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMouseDown = (e: MouseEvent) => {
|
||||||
|
if (store.editMode === 'remove') {
|
||||||
|
startDrawing(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
isDragging.value = true;
|
||||||
|
dragStart.value = { x: e.clientX, y: e.clientY };
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMouseMove = (e: MouseEvent) => {
|
||||||
|
if (store.editMode === 'remove') {
|
||||||
|
draw(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isDragging.value || !store.selectedImage || imageRect.value.width === 0) return;
|
||||||
|
|
||||||
|
const rect = imageRect.value;
|
||||||
|
|
||||||
|
const deltaX = (e.clientX - dragStart.value.x) / rect.width;
|
||||||
|
const deltaY = (e.clientY - dragStart.value.y) / rect.height;
|
||||||
|
|
||||||
|
// Update manual position
|
||||||
|
let newX = position.value.x + deltaX;
|
||||||
|
let newY = position.value.y + deltaY;
|
||||||
|
|
||||||
|
// Clamp logic
|
||||||
|
const padding = 0.005;
|
||||||
|
newX = Math.max(padding, Math.min(1 - padding, newX));
|
||||||
|
newY = Math.max(padding, Math.min(1 - padding, newY));
|
||||||
|
|
||||||
|
// Set ONLY for this image
|
||||||
|
store.setImageManualPosition(store.selectedIndex, newX, newY);
|
||||||
|
|
||||||
|
dragStart.value = { x: e.clientX, y: e.clientY };
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const onMouseUp = () => {
|
||||||
|
isDragging.value = false;
|
||||||
|
stopDrawing();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMouseLeave = () => {
|
||||||
|
isDragging.value = false;
|
||||||
|
stopDrawing();
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Drawing Logic ---
|
||||||
|
const startDrawing = (e: MouseEvent) => {
|
||||||
|
if (!canvasRef.value) return;
|
||||||
|
isDrawing.value = true;
|
||||||
|
currentPath.value = [];
|
||||||
|
draw(e);
|
||||||
|
};
|
||||||
|
|
||||||
|
const draw = (e: MouseEvent) => {
|
||||||
|
if (!isDrawing.value || !canvasRef.value) return;
|
||||||
|
const ctx = canvasRef.value.getContext('2d');
|
||||||
|
if (!ctx) return;
|
||||||
|
|
||||||
|
const rect = canvasRef.value.getBoundingClientRect();
|
||||||
|
const x = e.clientX - rect.left;
|
||||||
|
const y = e.clientY - rect.top;
|
||||||
|
|
||||||
|
// Normalize
|
||||||
|
const nx = x / imageRect.value.width;
|
||||||
|
const ny = y / imageRect.value.height;
|
||||||
|
|
||||||
|
currentPath.value.push({ x: nx, y: ny });
|
||||||
|
|
||||||
|
// Live feedback (Paint on top of existing)
|
||||||
|
ctx.fillStyle = 'rgba(255, 0, 0, 0.5)';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(x, y, store.brushSettings.size / 2, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
// Connect dots for smoothness if we have enough points
|
||||||
|
// (Optional optimization: draw line from last point)
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopDrawing = () => {
|
||||||
|
if (!isDrawing.value) return;
|
||||||
|
isDrawing.value = false;
|
||||||
|
|
||||||
|
if (currentPath.value.length > 0 && store.selectedIndex >= 0) {
|
||||||
|
const normWidth = store.brushSettings.size / imageRect.value.width;
|
||||||
|
store.addMaskStroke(store.selectedIndex, {
|
||||||
|
type: 'path',
|
||||||
|
points: [...currentPath.value],
|
||||||
|
width: normWidth
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="w-full h-full flex items-center justify-center bg-black relative p-4 overflow-hidden">
|
|
||||||
<div v-if="store.selectedImage" class="relative inline-flex justify-center items-center" style="max-width: 100%; max-height: 100%;">
|
|
||||||
<img
|
|
||||||
:src="convertFileSrc(store.selectedImage.path)"
|
|
||||||
class="max-w-full max-h-full w-auto h-auto block shadow-lg"
|
|
||||||
style="max-height: calc(100vh - 10rem);"
|
|
||||||
alt="Hero Image"
|
|
||||||
/>
|
|
||||||
<!-- Watermark Overlay Placeholder -->
|
|
||||||
<div
|
<div
|
||||||
v-if="store.selectedImage.zcaSuggestion"
|
ref="parentRef"
|
||||||
class="absolute border-2 border-dashed border-green-400 text-green-400 px-4 py-2 bg-black/50 pointer-events-none transition-all duration-500"
|
class="absolute inset-0 flex items-center justify-center bg-black overflow-hidden"
|
||||||
|
@mousemove="onMouseMove"
|
||||||
|
@mouseup="onMouseUp"
|
||||||
|
@mouseleave="onMouseLeave"
|
||||||
|
>
|
||||||
|
<!--
|
||||||
|
Dynamic Wrapper:
|
||||||
|
Dimensions strictly equal to the rendered image size.
|
||||||
|
This serves as the coordinate system for the watermark.
|
||||||
|
-->
|
||||||
|
<div
|
||||||
|
v-if="store.selectedImage"
|
||||||
|
class="relative shadow-2xl"
|
||||||
:style="{
|
:style="{
|
||||||
left: (store.selectedImage.zcaSuggestion.x * 100) + '%',
|
width: imageRect.width + 'px',
|
||||||
top: (store.selectedImage.zcaSuggestion.y * 100) + '%',
|
height: imageRect.height + 'px'
|
||||||
transform: 'translate(-50%, -50%)'
|
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
Smart Watermark ({{ store.selectedImage.zcaSuggestion.zone }})
|
<img
|
||||||
</div>
|
ref="imgRef"
|
||||||
|
:src="convertFileSrc(store.selectedImage.path)"
|
||||||
|
class="block w-full h-full select-none pointer-events-none"
|
||||||
|
alt="Hero Image"
|
||||||
|
loading="eager"
|
||||||
|
decoding="sync"
|
||||||
|
fetchpriority="high"
|
||||||
|
@load="calculateLayout"
|
||||||
|
@error="(e) => console.error('Hero Image Load Error:', e)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Text Watermark Overlay (Only in Add Mode) -->
|
||||||
<div
|
<div
|
||||||
v-else
|
v-if="store.editMode === 'add' && store.watermarkSettings.text"
|
||||||
class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 border-2 border-dashed border-white text-white px-4 py-2 bg-black/50 pointer-events-none"
|
class="absolute cursor-move select-none whitespace-nowrap font-medium"
|
||||||
|
:style="{
|
||||||
|
left: (position.x * 100) + '%',
|
||||||
|
top: (position.y * 100) + '%',
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
opacity: effectiveOpacity,
|
||||||
|
color: effectiveColor,
|
||||||
|
/* Scale based on HEIGHT of the IMAGE */
|
||||||
|
fontSize: (imageRect.height * effectiveScale) + 'px',
|
||||||
|
fontFamily: 'Roboto, sans-serif',
|
||||||
|
height: '0px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
zIndex: 10
|
||||||
|
}"
|
||||||
|
@mousedown="onMouseDown"
|
||||||
>
|
>
|
||||||
Calculating...
|
<span>
|
||||||
|
{{ store.watermarkSettings.text }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- Selection Ring when dragging -->
|
||||||
|
<div v-if="isDragging" class="absolute -inset-2 border border-blue-500 rounded-sm"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Canvas Overlay (Only in Remove Mode) -->
|
||||||
|
<canvas
|
||||||
|
v-show="store.editMode === 'remove'"
|
||||||
|
ref="canvasRef"
|
||||||
|
:width="imageRect.width"
|
||||||
|
:height="imageRect.height"
|
||||||
|
class="absolute inset-0 z-20 cursor-crosshair touch-none"
|
||||||
|
@mousedown="onMouseDown"
|
||||||
|
></canvas>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="text-gray-500">
|
<div v-else class="text-gray-500 flex flex-col items-center">
|
||||||
No image selected
|
<p>未选择图片</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.image-container {
|
||||||
|
/* Removed container-type: size to prevent layout collapse */
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
text-shadow:
|
||||||
|
-1px -1px 0 #000,
|
||||||
|
1px -1px 0 #000,
|
||||||
|
-1px 1px 0 #000,
|
||||||
|
1px 1px 0 #000;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
281
src/components/SettingsPanel.vue
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useGalleryStore } from "../stores/gallery";
|
||||||
|
import { Settings, CheckSquare, Type, Palette, Copy, Eraser, PlusSquare, Brush, Sparkles, Trash2, RotateCw, RotateCcw } from 'lucide-vue-next';
|
||||||
|
import { computed } from "vue";
|
||||||
|
|
||||||
|
const store = useGalleryStore();
|
||||||
|
|
||||||
|
// Computed properties to handle "Get from Image OR Global" and "Set to Image" logic
|
||||||
|
const currentScale = computed({
|
||||||
|
get: () => store.selectedImage?.scale ?? store.watermarkSettings.scale,
|
||||||
|
set: (val) => {
|
||||||
|
if (store.selectedIndex >= 0) {
|
||||||
|
store.setImageSetting(store.selectedIndex, 'scale', val);
|
||||||
|
} else {
|
||||||
|
store.updateWatermarkSettings({ scale: val });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentOpacity = computed({
|
||||||
|
get: () => store.selectedImage?.opacity ?? store.watermarkSettings.opacity,
|
||||||
|
set: (val) => {
|
||||||
|
if (store.selectedIndex >= 0) {
|
||||||
|
store.setImageSetting(store.selectedIndex, 'opacity', val);
|
||||||
|
} else {
|
||||||
|
store.updateWatermarkSettings({ opacity: val });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentColor = computed({
|
||||||
|
get: () => store.selectedImage?.color ?? store.watermarkSettings.color,
|
||||||
|
set: (val) => {
|
||||||
|
if (store.selectedIndex >= 0) {
|
||||||
|
store.setImageSetting(store.selectedIndex, 'color', val);
|
||||||
|
} else {
|
||||||
|
store.updateWatermarkSettings({ color: val });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const applyAll = () => {
|
||||||
|
if (confirm("是否将当前设置(大小、透明度、颜色)应用到所有图片?")) {
|
||||||
|
store.applySettingsToAll();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="h-full bg-gray-800 text-white flex flex-col w-80 border-l border-gray-700">
|
||||||
|
|
||||||
|
<!-- Mode Switcher Tabs -->
|
||||||
|
<div class="flex border-b border-gray-700">
|
||||||
|
<button
|
||||||
|
@click="store.editMode = 'add'"
|
||||||
|
class="flex-1 py-3 text-sm font-medium flex items-center justify-center gap-2 transition-colors"
|
||||||
|
:class="store.editMode === 'add' ? 'bg-gray-700 text-blue-400 border-b-2 border-blue-400' : 'text-gray-400 hover:bg-gray-750'"
|
||||||
|
>
|
||||||
|
<PlusSquare class="w-4 h-4" /> 添加
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="store.editMode = 'remove'"
|
||||||
|
class="flex-1 py-3 text-sm font-medium flex items-center justify-center gap-2 transition-colors"
|
||||||
|
:class="store.editMode === 'remove' ? 'bg-gray-700 text-red-400 border-b-2 border-red-400' : 'text-gray-400 hover:bg-gray-750'"
|
||||||
|
>
|
||||||
|
<Eraser class="w-4 h-4" /> 移除
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Content Area -->
|
||||||
|
<div class="flex-1 overflow-y-auto p-4 flex flex-col gap-6">
|
||||||
|
|
||||||
|
<!-- ADD MODE SETTINGS -->
|
||||||
|
<div v-if="store.editMode === 'add'" class="flex flex-col gap-6">
|
||||||
|
<h2 class="text-lg font-bold flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Settings class="w-5 h-5" />
|
||||||
|
水印设置
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
@click="applyAll"
|
||||||
|
title="应用设置到所有图片"
|
||||||
|
class="bg-gray-700 hover:bg-gray-600 p-1.5 rounded text-xs text-blue-300 transition-colors flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<Copy class="w-3 h-3" /> 全部
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<!-- Text Input -->
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="text-sm text-gray-400 uppercase tracking-wider font-semibold">水印内容</label>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<div class="relative flex-1">
|
||||||
|
<Type class="absolute left-3 top-2.5 w-4 h-4 text-gray-500" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
v-model="store.watermarkSettings.text"
|
||||||
|
class="w-full bg-gray-700 text-white pl-10 pr-3 py-2 rounded border border-gray-600 focus:border-blue-500 focus:outline-none"
|
||||||
|
placeholder="输入水印文字..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
@click="store.recalcAllWatermarks()"
|
||||||
|
class="bg-blue-600 hover:bg-blue-500 text-white p-2 rounded flex items-center justify-center transition-colors"
|
||||||
|
title="应用并重新计算所有图片布局"
|
||||||
|
>
|
||||||
|
<RotateCw class="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Color Picker -->
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="text-sm text-gray-400 uppercase tracking-wider font-semibold">字体颜色</label>
|
||||||
|
<div class="flex items-center gap-2 bg-gray-700 p-2 rounded border border-gray-600">
|
||||||
|
<Palette class="w-4 h-4 text-gray-400" />
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
v-model="currentColor"
|
||||||
|
class="w-8 h-8 rounded cursor-pointer bg-transparent border-none p-0"
|
||||||
|
/>
|
||||||
|
<span class="text-xs text-gray-300 font-mono">{{ currentColor }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Controls -->
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<div class="flex justify-between mb-1">
|
||||||
|
<label class="text-xs text-gray-400">字体大小 (比例)</label>
|
||||||
|
<span class="text-xs text-gray-300">{{ (currentScale * 100).toFixed(1) }}%</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="0.01"
|
||||||
|
max="0.20"
|
||||||
|
step="0.001"
|
||||||
|
v-model.number="currentScale"
|
||||||
|
class="w-full h-1 bg-gray-600 rounded-lg appearance-none cursor-pointer accent-blue-500"
|
||||||
|
/>
|
||||||
|
<p class="text-[10px] text-gray-500 mt-1">基于图片高度的比例</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="flex justify-between mb-1">
|
||||||
|
<label class="text-xs text-gray-400">不透明度</label>
|
||||||
|
<span class="text-xs text-gray-300">{{ (currentOpacity * 100).toFixed(0) }}%</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="0.1"
|
||||||
|
max="1.0"
|
||||||
|
step="0.01"
|
||||||
|
v-model.number="currentOpacity"
|
||||||
|
class="w-full h-1 bg-gray-600 rounded-lg appearance-none cursor-pointer accent-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Placement Info -->
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="text-sm text-gray-400 uppercase tracking-wider font-semibold">位置状态</label>
|
||||||
|
<div class="flex items-center gap-2 p-2 rounded bg-gray-700/50 border border-gray-600">
|
||||||
|
<div class="p-1 rounded bg-green-500/20 text-green-400">
|
||||||
|
<CheckSquare class="w-4 h-4" v-if="!store.selectedImage?.manualPosition" />
|
||||||
|
<div class="w-4 h-4" v-else></div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1">
|
||||||
|
<p class="text-sm font-medium text-gray-200" v-if="!store.selectedImage?.manualPosition">自动 (ZCA)</p>
|
||||||
|
<p class="text-sm font-medium text-blue-300" v-else>手动调整</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- REMOVE MODE SETTINGS -->
|
||||||
|
<div v-else class="flex flex-col gap-6">
|
||||||
|
<h2 class="text-lg font-bold flex items-center gap-2 text-red-400">
|
||||||
|
<Brush class="w-5 h-5" />
|
||||||
|
魔法橡皮擦
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<!-- Auto Detect Controls -->
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="text-sm text-gray-400 uppercase tracking-wider font-semibold">自动检测水印</label>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button
|
||||||
|
@click="store.detectCurrentWatermark()"
|
||||||
|
class="flex-1 bg-gray-700 hover:bg-gray-600 text-white py-2 rounded flex items-center justify-center gap-2 text-sm transition-colors"
|
||||||
|
:disabled="store.isDetecting || store.selectedIndex < 0"
|
||||||
|
>
|
||||||
|
<div v-if="store.isDetecting" class="w-3 h-3 border-2 border-white/30 border-t-white rounded-full animate-spin"></div>
|
||||||
|
<Sparkles v-else class="w-4 h-4" />
|
||||||
|
当前
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="store.detectAllWatermarks()"
|
||||||
|
class="flex-1 bg-gray-700 hover:bg-gray-600 text-white py-2 rounded flex items-center justify-center gap-2 text-sm transition-colors"
|
||||||
|
:disabled="store.isDetecting || store.images.length === 0"
|
||||||
|
>
|
||||||
|
<div v-if="store.isDetecting" class="w-3 h-3 border-2 border-white/30 border-t-white rounded-full animate-spin"></div>
|
||||||
|
<Sparkles v-else class="w-4 h-4" />
|
||||||
|
全部
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="store.selectedIndex >= 0 && store.clearMask(store.selectedIndex)"
|
||||||
|
class="bg-gray-700 hover:bg-gray-600 text-gray-300 px-3 rounded flex items-center justify-center transition-colors"
|
||||||
|
title="清空当前遮罩"
|
||||||
|
:disabled="store.selectedIndex < 0"
|
||||||
|
>
|
||||||
|
<Trash2 class="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-xs text-gray-400">涂抹想要移除的水印,AI 将自动填充背景。</p>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="flex justify-between mb-1">
|
||||||
|
<label class="text-xs text-gray-400">画笔大小</label>
|
||||||
|
<span class="text-xs text-gray-300">{{ store.brushSettings.size }}px</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="5"
|
||||||
|
max="100"
|
||||||
|
step="1"
|
||||||
|
v-model.number="store.brushSettings.size"
|
||||||
|
class="w-full h-1 bg-gray-600 rounded-lg appearance-none cursor-pointer accent-red-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-3 bg-red-900/20 border border-red-900/50 rounded text-xs text-red-200 flex flex-col gap-1">
|
||||||
|
<span>AI 修复功能已就绪。</span>
|
||||||
|
<span v-if="store.isProcessing" class="text-yellow-300 animate-pulse">
|
||||||
|
正在处理: {{ store.progress.current }} / {{ store.progress.total }}
|
||||||
|
</span>
|
||||||
|
<span v-if="store.isDetecting" class="text-blue-300 animate-pulse">
|
||||||
|
正在检测: {{ store.progress.current }} / {{ store.progress.total }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Execution Controls -->
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="text-sm text-gray-400 uppercase tracking-wider font-semibold">执行移除</label>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button
|
||||||
|
@click="store.processInpainting(store.selectedIndex)"
|
||||||
|
class="flex-1 bg-red-600 hover:bg-red-500 text-white py-2 rounded flex items-center justify-center gap-2 text-sm transition-colors shadow-lg"
|
||||||
|
:disabled="store.isProcessing || store.selectedIndex < 0"
|
||||||
|
>
|
||||||
|
<div v-if="store.isProcessing" class="w-3 h-3 border-2 border-white/30 border-t-white rounded-full animate-spin"></div>
|
||||||
|
<Eraser v-else class="w-4 h-4" />
|
||||||
|
当前
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="store.processAllInpainting()"
|
||||||
|
class="flex-1 bg-red-800 hover:bg-red-700 text-white py-2 rounded flex items-center justify-center gap-2 text-sm transition-colors shadow"
|
||||||
|
:disabled="store.isProcessing || store.images.length === 0"
|
||||||
|
>
|
||||||
|
<div v-if="store.isProcessing" class="w-3 h-3 border-2 border-white/30 border-t-white rounded-full animate-spin"></div>
|
||||||
|
<Eraser v-else class="w-4 h-4" />
|
||||||
|
全部
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
v-if="store.selectedImage && store.selectedImage.path !== store.selectedImage.originalPath"
|
||||||
|
@click="store.selectedIndex >= 0 && store.restoreImage(store.selectedIndex)"
|
||||||
|
class="w-full bg-gray-700 hover:bg-gray-600 text-gray-300 py-2 rounded text-sm transition-colors flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
<RotateCcw class="w-4 h-4" />
|
||||||
|
还原原图
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -23,13 +23,19 @@ const onSelect = (index: number) => {
|
|||||||
>
|
>
|
||||||
<template #default="{ item, index }">
|
<template #default="{ item, index }">
|
||||||
<div
|
<div
|
||||||
class="h-full w-[100px] p-2 cursor-pointer transition-colors"
|
class="h-full w-25 p-2 cursor-pointer transition-colors"
|
||||||
:class="{'bg-blue-600': store.selectedIndex === index, 'hover:bg-gray-700': store.selectedIndex !== index}"
|
:class="{'bg-blue-600': store.selectedIndex === index, 'hover:bg-gray-700': store.selectedIndex !== index}"
|
||||||
@click="onSelect(index)"
|
@click="onSelect(index)"
|
||||||
>
|
>
|
||||||
<div class="h-full w-full bg-gray-800 flex items-center justify-center overflow-hidden rounded border border-gray-600">
|
<div class="h-full w-full bg-gray-800 flex items-center justify-center overflow-hidden rounded border border-gray-600">
|
||||||
<!-- Use actual thumbnail path later -->
|
<!-- Use generated thumbnail -->
|
||||||
<img :src="convertFileSrc(item.path)" class="w-full h-full object-cover" loading="lazy" />
|
<img
|
||||||
|
:src="convertFileSrc(item.thumbnail)"
|
||||||
|
class="w-full h-full object-cover"
|
||||||
|
loading="lazy"
|
||||||
|
decoding="async"
|
||||||
|
fetchpriority="low"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -2,19 +2,60 @@ import { defineStore } from "pinia";
|
|||||||
import { ref, computed } from "vue";
|
import { ref, computed } from "vue";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
|
||||||
|
export interface MaskStroke {
|
||||||
|
type: 'path' | 'rect';
|
||||||
|
points?: {x: number, y: number}[]; // Normalized
|
||||||
|
rect?: {x: number, y: number, w: number, h: number}; // Normalized
|
||||||
|
width?: number; // Normalized brush width for paths
|
||||||
|
}
|
||||||
|
|
||||||
export interface ImageItem {
|
export interface ImageItem {
|
||||||
path: string;
|
path: string;
|
||||||
thumbnail?: string;
|
originalPath: string;
|
||||||
|
thumbnail: string;
|
||||||
name: string;
|
name: string;
|
||||||
width?: number;
|
width?: number;
|
||||||
height?: number;
|
height?: number;
|
||||||
zcaSuggestion?: { x: number; y: number; zone: string };
|
zcaSuggestion?: { x: number; y: number; zone: string };
|
||||||
|
manualPosition?: { x: number; y: number };
|
||||||
|
scale?: number;
|
||||||
|
opacity?: number;
|
||||||
|
color?: string;
|
||||||
|
maskStrokes?: MaskStroke[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WatermarkSettings {
|
||||||
|
type: 'text';
|
||||||
|
text: string;
|
||||||
|
color: string;
|
||||||
|
opacity: number;
|
||||||
|
scale: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useGalleryStore = defineStore("gallery", () => {
|
export const useGalleryStore = defineStore("gallery", () => {
|
||||||
const images = ref<ImageItem[]>([]);
|
const images = ref<ImageItem[]>([]);
|
||||||
const selectedIndex = ref<number>(-1);
|
const selectedIndex = ref<number>(-1);
|
||||||
|
|
||||||
|
// 'add' = Add Watermark, 'remove' = Remove Watermark (Inpainting)
|
||||||
|
const editMode = ref<'add' | 'remove'>('add');
|
||||||
|
|
||||||
|
const watermarkSettings = ref<WatermarkSettings>({
|
||||||
|
type: 'text',
|
||||||
|
text: '水印',
|
||||||
|
color: '#FFFFFF',
|
||||||
|
opacity: 1.0,
|
||||||
|
scale: 0.03,
|
||||||
|
});
|
||||||
|
|
||||||
|
const brushSettings = ref({
|
||||||
|
size: 20, // screen pixels
|
||||||
|
opacity: 0.5 // visual only
|
||||||
|
});
|
||||||
|
|
||||||
|
const isDetecting = ref(false);
|
||||||
|
const isProcessing = ref(false);
|
||||||
|
const progress = ref({ current: 0, total: 0 });
|
||||||
|
|
||||||
const selectedImage = computed(() => {
|
const selectedImage = computed(() => {
|
||||||
if (selectedIndex.value >= 0 && selectedIndex.value < images.value.length) {
|
if (selectedIndex.value >= 0 && selectedIndex.value < images.value.length) {
|
||||||
return images.value[selectedIndex.value];
|
return images.value[selectedIndex.value];
|
||||||
@@ -23,10 +64,218 @@ export const useGalleryStore = defineStore("gallery", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function setImages(newImages: ImageItem[]) {
|
function setImages(newImages: ImageItem[]) {
|
||||||
images.value = newImages;
|
images.value = newImages.map(img => ({
|
||||||
|
...img,
|
||||||
|
// Ensure originalPath is set if not already present from backend
|
||||||
|
originalPath: img.originalPath || img.path
|
||||||
|
}));
|
||||||
selectedIndex.value = -1;
|
selectedIndex.value = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function restoreImage(index: number) {
|
||||||
|
const img = images.value[index];
|
||||||
|
if (img) {
|
||||||
|
img.path = img.originalPath;
|
||||||
|
img.maskStrokes = []; // Also clear any masks if restoring
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateWatermarkSettings(settings: Partial<WatermarkSettings>) {
|
||||||
|
watermarkSettings.value = { ...watermarkSettings.value, ...settings };
|
||||||
|
}
|
||||||
|
|
||||||
|
function setImageManualPosition(index: number, x: number, y: number) {
|
||||||
|
if (images.value[index]) {
|
||||||
|
images.value[index].manualPosition = { x, y };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setImageSetting(index: number, setting: 'scale' | 'opacity' | 'color', value: number | string) {
|
||||||
|
if (images.value[index]) {
|
||||||
|
// @ts-ignore
|
||||||
|
images.value[index][setting] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addMaskStroke(index: number, stroke: MaskStroke) {
|
||||||
|
if (images.value[index]) {
|
||||||
|
if (!images.value[index].maskStrokes) {
|
||||||
|
images.value[index].maskStrokes = [];
|
||||||
|
}
|
||||||
|
images.value[index].maskStrokes!.push(stroke);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearMask(index: number) {
|
||||||
|
if (images.value[index]) {
|
||||||
|
images.value[index].maskStrokes = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to run detection on a single image object
|
||||||
|
async function detectWatermarkForImage(img: ImageItem) {
|
||||||
|
try {
|
||||||
|
// Clear existing mask first
|
||||||
|
img.maskStrokes = [];
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
const result = await invoke<{rects: {x: number, y: number, width: number, height: number}[]}>("detect_watermark", { path: img.path });
|
||||||
|
|
||||||
|
if (result.rects && result.rects.length > 0) {
|
||||||
|
result.rects.forEach(r => {
|
||||||
|
img.maskStrokes!.push({
|
||||||
|
type: 'rect',
|
||||||
|
rect: { x: r.x, y: r.y, w: r.width, h: r.height }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Detection failed for ${img.name}`, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function detectCurrentWatermark() {
|
||||||
|
if (selectedIndex.value < 0 || !images.value[selectedIndex.value]) return;
|
||||||
|
isDetecting.value = true;
|
||||||
|
progress.value = { current: 0, total: 1 };
|
||||||
|
try {
|
||||||
|
await detectWatermarkForImage(images.value[selectedIndex.value]);
|
||||||
|
progress.value.current = 1;
|
||||||
|
} finally {
|
||||||
|
isDetecting.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function detectAllWatermarks() {
|
||||||
|
if (images.value.length === 0) return;
|
||||||
|
isDetecting.value = true;
|
||||||
|
progress.value = { current: 0, total: images.value.length };
|
||||||
|
|
||||||
|
try {
|
||||||
|
const batchSize = 5;
|
||||||
|
for (let i = 0; i < images.value.length; i += batchSize) {
|
||||||
|
const batch = images.value.slice(i, i + batchSize).map(async (img) => {
|
||||||
|
await detectWatermarkForImage(img);
|
||||||
|
progress.value.current++;
|
||||||
|
});
|
||||||
|
await Promise.all(batch);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
isDetecting.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function recalcAllWatermarks() {
|
||||||
|
if (images.value.length === 0) return;
|
||||||
|
|
||||||
|
const text = watermarkSettings.value.text;
|
||||||
|
const baseScale = watermarkSettings.value.scale;
|
||||||
|
|
||||||
|
// Process in batches to avoid overwhelming the backend
|
||||||
|
const batchSize = 5;
|
||||||
|
for (let i = 0; i < images.value.length; i += batchSize) {
|
||||||
|
const batch = images.value.slice(i, i + batchSize).map(async (img) => {
|
||||||
|
try {
|
||||||
|
const result = await invoke<{x: number, y: number, scale: number}>("layout_watermark", {
|
||||||
|
path: img.path,
|
||||||
|
text: text,
|
||||||
|
baseScale: baseScale
|
||||||
|
});
|
||||||
|
|
||||||
|
const idx = images.value.indexOf(img);
|
||||||
|
if (idx >= 0) {
|
||||||
|
setImageManualPosition(idx, result.x, result.y);
|
||||||
|
setImageSetting(idx, 'scale', result.scale);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Layout failed for ${img.name}`, e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await Promise.all(batch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal helper for single image inpainting logic
|
||||||
|
async function runInpaintingForImage(img: ImageItem) {
|
||||||
|
if (!img.maskStrokes || img.maskStrokes.length === 0) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newPath = await invoke<string>("run_inpainting", {
|
||||||
|
path: img.path,
|
||||||
|
strokes: img.maskStrokes
|
||||||
|
});
|
||||||
|
|
||||||
|
img.path = newPath;
|
||||||
|
img.maskStrokes = [];
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Inpainting failed", e);
|
||||||
|
throw e; // Propagate error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function processInpainting(index: number) {
|
||||||
|
const img = images.value[index];
|
||||||
|
if (!img) return;
|
||||||
|
|
||||||
|
isProcessing.value = true;
|
||||||
|
progress.value = { current: 0, total: 1 };
|
||||||
|
try {
|
||||||
|
await runInpaintingForImage(img);
|
||||||
|
progress.value.current = 1;
|
||||||
|
} catch (e) {
|
||||||
|
alert("处理失败: " + e);
|
||||||
|
} finally {
|
||||||
|
isProcessing.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function processAllInpainting() {
|
||||||
|
const candidates = images.value.filter(img => img.maskStrokes && img.maskStrokes.length > 0);
|
||||||
|
if (candidates.length === 0) return;
|
||||||
|
|
||||||
|
isProcessing.value = true;
|
||||||
|
progress.value = { current: 0, total: candidates.length };
|
||||||
|
try {
|
||||||
|
// Parallel processing in batches to avoid overwhelming backend
|
||||||
|
const batchSize = 4;
|
||||||
|
for (let i = 0; i < candidates.length; i += batchSize) {
|
||||||
|
const batch = candidates.slice(i, i + batchSize).map(async (img) => {
|
||||||
|
await runInpaintingForImage(img);
|
||||||
|
progress.value.current++;
|
||||||
|
});
|
||||||
|
await Promise.all(batch);
|
||||||
|
}
|
||||||
|
alert("批量处理完成!");
|
||||||
|
} catch (e) {
|
||||||
|
alert("批量处理部分失败: " + e);
|
||||||
|
} finally {
|
||||||
|
isProcessing.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Applies the settings from the CURRENT image (or global if not overridden) to ALL images
|
||||||
|
// Strategy: Update Global Settings to match current view, and clear individual overrides so everyone follows global.
|
||||||
|
function applySettingsToAll() {
|
||||||
|
const current = selectedImage.value;
|
||||||
|
if (!current) return;
|
||||||
|
|
||||||
|
const newScale = current.scale ?? watermarkSettings.value.scale;
|
||||||
|
const newOpacity = current.opacity ?? watermarkSettings.value.opacity;
|
||||||
|
const newColor = current.color ?? watermarkSettings.value.color;
|
||||||
|
|
||||||
|
// 1. Update Global
|
||||||
|
watermarkSettings.value.scale = newScale;
|
||||||
|
watermarkSettings.value.opacity = newOpacity;
|
||||||
|
watermarkSettings.value.color = newColor;
|
||||||
|
|
||||||
|
// 2. Clear overrides on ALL images
|
||||||
|
images.value.forEach(img => {
|
||||||
|
img.scale = undefined;
|
||||||
|
img.opacity = undefined;
|
||||||
|
img.color = undefined;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function selectImage(index: number) {
|
async function selectImage(index: number) {
|
||||||
if (index < 0 || index >= images.value.length) return;
|
if (index < 0 || index >= images.value.length) return;
|
||||||
selectedIndex.value = index;
|
selectedIndex.value = index;
|
||||||
@@ -35,8 +284,6 @@ export const useGalleryStore = defineStore("gallery", () => {
|
|||||||
if (!img.zcaSuggestion) {
|
if (!img.zcaSuggestion) {
|
||||||
try {
|
try {
|
||||||
const suggestion = await invoke<{x: number, y: number, zone: string}>("get_zca_suggestion", { path: img.path });
|
const suggestion = await invoke<{x: number, y: number, zone: string}>("get_zca_suggestion", { path: img.path });
|
||||||
// Update the item in the array
|
|
||||||
// Note: Directly modifying the object inside ref array is reactive in Vue 3
|
|
||||||
img.zcaSuggestion = suggestion;
|
img.zcaSuggestion = suggestion;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("ZCA failed", e);
|
console.error("ZCA failed", e);
|
||||||
@@ -44,11 +291,43 @@ export const useGalleryStore = defineStore("gallery", () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function nextImage() {
|
||||||
|
if (images.value.length === 0) return;
|
||||||
|
const nextIndex = (selectedIndex.value + 1) % images.value.length;
|
||||||
|
selectImage(nextIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
function prevImage() {
|
||||||
|
if (images.value.length === 0) return;
|
||||||
|
const prevIndex = (selectedIndex.value - 1 + images.value.length) % images.value.length;
|
||||||
|
selectImage(prevIndex);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
images,
|
images,
|
||||||
selectedIndex,
|
selectedIndex,
|
||||||
selectedImage,
|
selectedImage,
|
||||||
|
editMode,
|
||||||
|
watermarkSettings,
|
||||||
|
brushSettings,
|
||||||
|
isDetecting,
|
||||||
|
isProcessing,
|
||||||
|
progress,
|
||||||
setImages,
|
setImages,
|
||||||
selectImage,
|
selectImage,
|
||||||
|
updateWatermarkSettings,
|
||||||
|
setImageManualPosition,
|
||||||
|
setImageSetting,
|
||||||
|
applySettingsToAll,
|
||||||
|
addMaskStroke,
|
||||||
|
clearMask,
|
||||||
|
detectCurrentWatermark,
|
||||||
|
detectAllWatermarks,
|
||||||
|
recalcAllWatermarks,
|
||||||
|
processInpainting,
|
||||||
|
processAllInpainting,
|
||||||
|
restoreImage,
|
||||||
|
nextImage,
|
||||||
|
prevImage
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1 +1,28 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
src: url('/fonts/Roboto-Regular.ttf') format('truetype');
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom scrollbar styles */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background-color: #1f2937; /* gray-800 */
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background-color: #4b5563; /* gray-600 */
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background-color: #6b7280; /* gray-500 */
|
||||||
|
}
|
||||||