Compare commits

...

5 Commits

Author SHA1 Message Date
Julian Freeman
84ae92d1e3 fix export bug 2026-01-19 13:18:57 -04:00
Julian Freeman
6cb5b7a61f ocr cover fix 2026-01-19 12:58:05 -04:00
Julian Freeman
eb251b5eac ocr detect 2026-01-19 12:42:05 -04:00
Julian Freeman
6439759b04 support restore 2026-01-19 12:18:08 -04:00
Julian Freeman
f96033e421 lama remove 2026-01-19 12:08:36 -04:00
10 changed files with 874 additions and 199 deletions

3
.gitignore vendored
View File

@@ -22,3 +22,6 @@ dist-ssr
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?
*.onnx
*.dll

368
src-tauri/Cargo.lock generated
View File

@@ -212,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"
@@ -476,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"
@@ -499,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",
] ]
@@ -512,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",
] ]
@@ -665,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"
@@ -870,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"
@@ -885,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"
@@ -946,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"
@@ -953,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]]
@@ -967,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"
@@ -1417,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"
@@ -1944,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",
] ]
@@ -1974,6 +2037,16 @@ 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]] [[package]]
name = "libm" name = "libm"
version = "0.2.15" version = "0.2.15"
@@ -1990,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"
@@ -2020,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"
@@ -2165,6 +2250,53 @@ dependencies = [
"typenum", "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"
@@ -2553,12 +2685,81 @@ 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]] [[package]]
name = "owned_ttf_parser" name = "owned_ttf_parser"
version = "0.25.1" version = "0.25.1"
@@ -2628,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"
@@ -2825,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"
@@ -3335,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"
@@ -3365,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"
@@ -3422,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"
@@ -3699,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"
@@ -3858,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",
@@ -4180,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"
@@ -4572,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"
@@ -4632,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"
@@ -4783,6 +5122,8 @@ dependencies = [
"ab_glyph", "ab_glyph",
"image", "image",
"imageproc", "imageproc",
"ndarray 0.16.1",
"ort",
"rayon", "rayon",
"serde", "serde",
"serde_json", "serde_json",
@@ -4845,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"
@@ -5498,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"

View File

@@ -26,4 +26,6 @@ rayon = "1.10"
tauri-plugin-dialog = "2" tauri-plugin-dialog = "2"
imageproc = "0.25" imageproc = "0.25"
ab_glyph = "0.2.23" ab_glyph = "0.2.23"
ort = { version = "=2.0.0-rc.11", features = ["load-dynamic"] }
ndarray = "0.16"

139
src-tauri/src/lama.rs Normal file
View File

@@ -0,0 +1,139 @@
use image::{Rgba, RgbaImage};
use ort::session::{Session, builder::GraphOptimizationLevel};
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 = Session::builder()
.map_err(|e| format!("Failed to create session builder: {}", e))?
.with_optimization_level(GraphOptimizationLevel::Level3)
.map_err(|e| format!("Failed to set opt level: {}", e))?
.with_intra_threads(4)
.map_err(|e| format!("Failed to set threads: {}", e))?
.commit_from_file(model_path)
.map_err(|e| format!("Failed to load model from {:?}: {}", model_path, 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)
}

View File

@@ -92,6 +92,10 @@ use rayon::prelude::*;
use std::path::Path; use std::path::Path;
use imageproc::drawing::draw_text_mut; use imageproc::drawing::draw_text_mut;
use ab_glyph::{FontRef, PxScale}; use ab_glyph::{FontRef, PxScale};
use tauri::{AppHandle, Manager};
mod lama;
mod ocr;
// Embed the font to ensure it's always available without path issues // Embed the font to ensure it's always available without path issues
const FONT_DATA: &[u8] = include_bytes!("../assets/fonts/Roboto-Regular.ttf"); const FONT_DATA: &[u8] = include_bytes!("../assets/fonts/Roboto-Regular.ttf");
@@ -140,7 +144,7 @@ fn parse_hex_color(hex: &str) -> image::Rgba<u8> {
} }
#[tauri::command] #[tauri::command]
async fn export_batch(images: Vec<ExportImageTask>, watermark: WatermarkSettings, output_dir: String) -> Result<String, String> { async fn export_batch(images: Vec<ExportImageTask>, watermark: WatermarkSettings, output_dir: String, mode: String) -> Result<String, String> {
let font = FontRef::try_from_slice(FONT_DATA).map_err(|e| format!("Font error: {}", e))?; let font = FontRef::try_from_slice(FONT_DATA).map_err(|e| format!("Font error: {}", e))?;
// Note: Settings are now resolved per-task // Note: Settings are now resolved per-task
@@ -153,6 +157,8 @@ async fn export_batch(images: Vec<ExportImageTask>, watermark: WatermarkSettings
let mut base_img = dynamic_img.to_rgba8(); let mut base_img = dynamic_img.to_rgba8();
let (width, height) = base_img.dimensions(); let (width, height) = base_img.dimensions();
// ONLY EXECUTE WATERMARK LOGIC IF MODE IS 'ADD'
if mode == "add" {
// Determine effective settings (Task > Global) // Determine effective settings (Task > Global)
let eff_scale = task.scale.unwrap_or(watermark.scale); let eff_scale = task.scale.unwrap_or(watermark.scale);
let eff_opacity = task.opacity.unwrap_or(watermark.opacity); let eff_opacity = task.opacity.unwrap_or(watermark.opacity);
@@ -214,6 +220,8 @@ async fn export_batch(images: Vec<ExportImageTask>, watermark: WatermarkSettings
x = x.max(0); x = x.max(0);
y = y.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) // 6. Draw Stroke (Simple 4-direction offset for black outline)
// Stroke alpha should match text alpha // Stroke alpha should match text alpha
let stroke_color = image::Rgba([0, 0, 0, text_color[3]]); let stroke_color = image::Rgba([0, 0, 0, text_color[3]]);
@@ -239,6 +247,8 @@ async fn export_batch(images: Vec<ExportImageTask>, watermark: WatermarkSettings
&font, &font,
&watermark.text, &watermark.text,
); );
}
} // END IF MODE == ADD
// Save // Save
let file_name = input_path.file_name().unwrap_or_default(); let file_name = input_path.file_name().unwrap_or_default();
@@ -450,52 +460,63 @@ struct Rect {
} }
#[tauri::command] #[tauri::command]
async fn detect_watermark(path: String) -> Result<DetectionResult, String> { async fn detect_watermark(app: AppHandle, path: String) -> Result<DetectionResult, String> {
let img = image::open(&path).map_err(|e| e.to_string())?; let img = image::open(&path).map_err(|e| e.to_string())?.to_rgba8();
let (width, height) = img.dimensions();
let gray = img.to_luma8();
// "Stroke Detection" Algorithm // 1. Try OCR Detection
// Distinguishes "Text" (Thin White Strokes) from "Solid White Areas" (Walls, Sky) let ocr_model_path = app.path().resource_dir()
// Logic: A white text pixel must be "sandwiched" by dark pixels within a short distance. .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 cell_size = 10;
let grid_w = (width + cell_size - 1) / cell_size; let grid_w = (width + cell_size - 1) / cell_size;
let grid_h = (height + 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 mut grid = vec![false; (grid_w * grid_h) as usize];
// Focus Areas: Top 15%, Bottom 25%
let top_limit = (height as f64 * 0.15) as u32; let top_limit = (height as f64 * 0.15) as u32;
let bottom_start = (height as f64 * 0.75) as u32; let bottom_start = (height as f64 * 0.75) as u32;
let max_stroke_width = 15;
let max_stroke_width = 15; // Max pixels for a text stroke thickness let contrast_threshold = 40;
let contrast_threshold = 40; // How much darker the background must be let brightness_threshold = 200;
let brightness_threshold = 200; // Text must be at least this white
for y in 1..height-1 { for y in 1..height-1 {
if y > top_limit && y < bottom_start { continue; } if y > top_limit && y < bottom_start { continue; }
for x in 1..width-1 { for x in 1..width-1 {
let p = gray.get_pixel(x, y)[0]; let p = gray.get_pixel(x, y)[0];
// 1. Must be Bright
if p < brightness_threshold { continue; } if p < brightness_threshold { continue; }
// 2. Stroke Check
// We check for "Vertical Stroke" (Dark - Bright - Dark vertically)
// OR "Horizontal Stroke" (Dark - Bright - Dark horizontally)
let mut is_stroke = false; let mut is_stroke = false;
// Check Horizontal Stroke (Vertical boundaries? No, Vertical Stroke has Left/Right boundaries?
// Terminology: "Vertical Stroke" is like 'I'. It has Left/Right boundaries.
// "Horizontal Stroke" is like '-', It has Up/Down boundaries.
// Let's check Left/Right boundaries (Vertical Stroke)
let mut left_bound = false; let mut left_bound = false;
let mut right_bound = false; let mut right_bound = false;
// Search Left
for k in 1..=max_stroke_width { for k in 1..=max_stroke_width {
if x < k { break; } if x < k { break; }
let neighbor = gray.get_pixel(x - k, y)[0]; let neighbor = gray.get_pixel(x - k, y)[0];
@@ -504,7 +525,6 @@ async fn detect_watermark(path: String) -> Result<DetectionResult, String> {
break; break;
} }
} }
// Search Right
if left_bound { if left_bound {
for k in 1..=max_stroke_width { for k in 1..=max_stroke_width {
if x + k >= width { break; } if x + k >= width { break; }
@@ -519,11 +539,8 @@ async fn detect_watermark(path: String) -> Result<DetectionResult, String> {
if left_bound && right_bound { if left_bound && right_bound {
is_stroke = true; is_stroke = true;
} else { } else {
// Check Up/Down boundaries (Horizontal Stroke)
let mut up_bound = false; let mut up_bound = false;
let mut down_bound = false; let mut down_bound = false;
// Search Up
for k in 1..=max_stroke_width { for k in 1..=max_stroke_width {
if y < k { break; } if y < k { break; }
let neighbor = gray.get_pixel(x, y - k)[0]; let neighbor = gray.get_pixel(x, y - k)[0];
@@ -532,7 +549,6 @@ async fn detect_watermark(path: String) -> Result<DetectionResult, String> {
break; break;
} }
} }
// Search Down
if up_bound { if up_bound {
for k in 1..=max_stroke_width { for k in 1..=max_stroke_width {
if y + k >= height { break; } if y + k >= height { break; }
@@ -543,10 +559,7 @@ async fn detect_watermark(path: String) -> Result<DetectionResult, String> {
} }
} }
} }
if up_bound && down_bound { is_stroke = true; }
if up_bound && down_bound {
is_stroke = true;
}
} }
if is_stroke { if is_stroke {
@@ -557,7 +570,6 @@ async fn detect_watermark(path: String) -> Result<DetectionResult, String> {
} }
} }
// Connected Components on Grid (Simple merging)
let mut rects = Vec::new(); let mut rects = Vec::new();
let mut visited = vec![false; grid.len()]; let mut visited = vec![false; grid.len()];
@@ -565,7 +577,6 @@ async fn detect_watermark(path: String) -> Result<DetectionResult, String> {
for gx in 0..grid_w { for gx in 0..grid_w {
let idx = (gy * grid_w + gx) as usize; let idx = (gy * grid_w + gx) as usize;
if grid[idx] && !visited[idx] { if grid[idx] && !visited[idx] {
// Start a new component
let mut min_gx = gx; let mut min_gx = gx;
let mut max_gx = gx; let mut max_gx = gx;
let mut min_gy = gy; let mut min_gy = gy;
@@ -580,7 +591,6 @@ async fn detect_watermark(path: String) -> Result<DetectionResult, String> {
if cy < min_gy { min_gy = cy; } if cy < min_gy { min_gy = cy; }
if cy > max_gy { max_gy = cy; } if cy > max_gy { max_gy = cy; }
// Neighbors
let neighbors = [ let neighbors = [
(cx.wrapping_sub(1), cy), (cx + 1, cy), (cx.wrapping_sub(1), cy), (cx + 1, cy),
(cx, cy.wrapping_sub(1)), (cx, cy + 1) (cx, cy.wrapping_sub(1)), (cx, cy + 1)
@@ -597,8 +607,6 @@ async fn detect_watermark(path: String) -> Result<DetectionResult, String> {
} }
} }
// Convert grid rect to normalized image rect
// Add padding (1 cell)
let px = (min_gx * cell_size) as f64; let px = (min_gx * cell_size) as f64;
let py = (min_gy * cell_size) as f64; let py = (min_gy * cell_size) as f64;
let pw = ((max_gx - min_gx + 1) * cell_size) as f64; let pw = ((max_gx - min_gx + 1) * cell_size) as f64;
@@ -646,12 +654,12 @@ enum MaskStroke {
} }
#[tauri::command] #[tauri::command]
async fn run_inpainting(path: String, strokes: Vec<MaskStroke>) -> Result<String, String> { async fn run_inpainting(app: AppHandle, path: String, strokes: Vec<MaskStroke>) -> Result<String, String> {
let img = image::open(&path).map_err(|e| e.to_string())?.to_rgba8(); let img = image::open(&path).map_err(|e| e.to_string())?.to_rgba8();
let (width, height) = img.dimensions(); let (width, height) = img.dimensions();
// 1. Create Mask // 1. Create Gray Mask (0 = keep, 255 = remove)
let mut mask = vec![false; (width * height) as usize]; let mut mask = image::GrayImage::new(width, height);
for stroke in strokes { for stroke in strokes {
match stroke { match stroke {
@@ -661,10 +669,11 @@ async fn run_inpainting(path: String, strokes: Vec<MaskStroke>) -> Result<String
let w = (rect.w * width as f64) as i32; let w = (rect.w * width as f64) as i32;
let h = (rect.h * height as f64) as i32; let h = (rect.h * height as f64) as i32;
// Draw 255 on mask
for y in y1..(y1 + h) { for y in y1..(y1 + h) {
for x in x1..(x1 + w) { for x in x1..(x1 + w) {
if x >= 0 && x < width as i32 && y >= 0 && y < height as i32 { if x >= 0 && x < width as i32 && y >= 0 && y < height as i32 {
mask[(y as u32 * width + x as u32) as usize] = true; mask.put_pixel(x as u32, y as u32, image::Luma([255]));
} }
} }
} }
@@ -699,7 +708,7 @@ async fn run_inpainting(path: String, strokes: Vec<MaskStroke>) -> Result<String
let nx = cx + dx; let nx = cx + dx;
let ny = cy + dy; let ny = cy + dy;
if nx >= 0 && nx < width as i32 && ny >= 0 && ny < height as i32 { if nx >= 0 && nx < width as i32 && ny >= 0 && ny < height as i32 {
mask[(ny as u32 * width + nx as u32) as usize] = true; mask.put_pixel(nx as u32, ny as u32, image::Luma([255]));
} }
} }
} }
@@ -710,76 +719,26 @@ async fn run_inpainting(path: String, strokes: Vec<MaskStroke>) -> Result<String
} }
} }
// 2. Diffusion Inpainting (Simple) // 2. Resolve Model Path
// Iteratively replace masked pixels with average of non-masked neighbors let model_path = app.path().resource_dir()
// To make it converge, we update 'mask' as we go (treating filled pixels as valid source) .map_err(|e| e.to_string())?
// But standard diffusion uses double buffering. .join("resources")
// For "Removing Text", simple inward filling works well. .join("lama_fp32.onnx");
let iterations = 30; if !model_path.exists() {
let mut current_img = img.clone(); return Err("Model file 'lama_fp32.onnx' not found in resources.".to_string());
let mut next_img = img.clone();
// Convert mask to a distance map-like state?
// Or just simple neighbor average.
for _ in 0..iterations {
let mut changed = false;
for y in 0..height {
for x in 0..width {
let idx = (y * width + x) as usize;
if mask[idx] {
// It's a hole. Find valid neighbors.
// Valid = Not in ORIGINAL mask (so we pull from original image)
// OR processed in previous iteration?
// Simple logic: Pull from 'current_img'.
let mut sum_r = 0u32;
let mut sum_g = 0u32;
let mut sum_b = 0u32;
let mut count = 0;
// Check 4 neighbors
let neighbors = [
(x.wrapping_sub(1), y), (x + 1, y),
(x, y.wrapping_sub(1)), (x, y + 1)
];
for (nx, ny) in neighbors {
if nx < width && ny < height {
// Weighted check: If neighbor is ALSO masked, it contributes less?
// Or just take everything.
let pixel = current_img.get_pixel(nx, ny);
sum_r += pixel[0] as u32;
sum_g += pixel[1] as u32;
sum_b += pixel[2] as u32;
count += 1;
}
} }
if count > 0 { // 3. Run Inference
let avg = image::Rgba([ // This is computationally heavy, maybe run in thread? Tauri async commands are already threaded.
(sum_r / count) as u8, let result_img = lama::run_lama_inpainting(&model_path, &img, &mask)?;
(sum_g / count) as u8,
(sum_b / count) as u8,
255
]);
next_img.put_pixel(x, y, avg);
changed = true;
}
}
}
}
current_img = next_img.clone();
if !changed { break; }
}
// Save to temp // Save to temp
let cache_dir = get_cache_dir(); 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 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); let out_path = cache_dir.join(file_name);
current_img.save(&out_path).map_err(|e| e.to_string())?; result_img.save(&out_path).map_err(|e| e.to_string())?;
Ok(out_path.to_string_lossy().to_string()) Ok(out_path.to_string_lossy().to_string())
} }

189
src-tauri/src/ocr.rs Normal file
View File

@@ -0,0 +1,189 @@
use image::RgbaImage;
use ort::session::{Session, builder::GraphOptimizationLevel};
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
let mut session = Session::builder()
.map_err(|e| format!("Failed to create session: {}", e))?
.with_optimization_level(GraphOptimizationLevel::Level3)
.map_err(|e| format!("Failed to set opt: {}", e))?
.with_intra_threads(4)
.map_err(|e| format!("Failed to set threads: {}", e))?
.commit_from_file(model_path)
.map_err(|e| format!("Failed to load OCR model: {}", 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();
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(input_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, y)];
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)
}

View File

@@ -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",

View File

@@ -32,7 +32,9 @@ async function openFolder() {
async function exportBatch() { async function exportBatch() {
if (store.images.length === 0) return; if (store.images.length === 0) return;
if (!store.watermarkSettings.text) {
// Only require text if in ADD mode
if (store.editMode === 'add' && !store.watermarkSettings.text) {
alert("请输入水印文字。"); alert("请输入水印文字。");
return; return;
} }
@@ -67,7 +69,8 @@ async function exportBatch() {
await invoke('export_batch', { await invoke('export_batch', {
images: exportTasks, images: exportTasks,
watermark: rustWatermarkSettings, watermark: rustWatermarkSettings,
outputDir: outputDir outputDir: outputDir,
mode: store.editMode
}); });
alert("批量导出完成!"); alert("批量导出完成!");
} }

View File

@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { useGalleryStore } from "../stores/gallery"; import { useGalleryStore } from "../stores/gallery";
import { Settings, CheckSquare, Type, Palette, Copy, Eraser, PlusSquare, Brush, Sparkles, Trash2, RotateCw } from 'lucide-vue-next'; import { Settings, CheckSquare, Type, Palette, Copy, Eraser, PlusSquare, Brush, Sparkles, Trash2, RotateCw, RotateCcw } from 'lucide-vue-next';
import { computed } from "vue"; import { computed } from "vue";
const store = useGalleryStore(); const store = useGalleryStore();
@@ -227,6 +227,15 @@ const applyAll = () => {
<Eraser class="w-5 h-5" /> <Eraser class="w-5 h-5" />
执行移除 执行移除
</button> </button>
<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> </div>

View File

@@ -11,6 +11,7 @@ export interface MaskStroke {
export interface ImageItem { export interface ImageItem {
path: string; path: string;
originalPath: string;
thumbnail: string; thumbnail: string;
name: string; name: string;
width?: number; width?: number;
@@ -59,10 +60,22 @@ 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>) { function updateWatermarkSettings(settings: Partial<WatermarkSettings>) {
watermarkSettings.value = { ...watermarkSettings.value, ...settings }; watermarkSettings.value = { ...watermarkSettings.value, ...settings };
} }
@@ -238,6 +251,7 @@ export const useGalleryStore = defineStore("gallery", () => {
clearMask, clearMask,
detectAllWatermarks, detectAllWatermarks,
recalcAllWatermarks, recalcAllWatermarks,
processInpainting processInpainting,
restoreImage
}; };
}); });