refactor: CLI P0-P2 全面改进

P0:
- 子命令重构: qrgen encode / qrgen decode
- Shell 补全: --generate-completions bash/zsh/fish/pwsh/elvish
- -v 改为 -V (version), Cli::version 继承 Cargo.toml

P1:
- stdin 管道: encode/decode 支持 - 从 stdin 读取
- 退出码: 0=成功, 1=输入错误, 2=系统错误
- 错误消息: bail! 宏 + 上下文信息

P2:
- 进度指示: batch 模式使用 indicatif 进度条
- --version 自动生成

新增依赖: clap_complete, indicatif, image (direct)
This commit is contained in:
2026-06-20 17:48:17 +08:00
parent 5fb967a353
commit e6a7efc760
3 changed files with 824 additions and 437 deletions
Generated
+587
View File
@@ -17,6 +17,24 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "aligned"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685"
dependencies = [
"as-slice",
]
[[package]]
name = "aligned-vec"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b"
dependencies = [
"equator",
]
[[package]] [[package]]
name = "alloc-no-stdlib" name = "alloc-no-stdlib"
version = "2.0.4" version = "2.0.4"
@@ -97,6 +115,12 @@ version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]]
name = "arbitrary"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
[[package]] [[package]]
name = "arboard" name = "arboard"
version = "3.6.1" version = "3.6.1"
@@ -118,6 +142,32 @@ dependencies = [
"x11rb", "x11rb",
] ]
[[package]]
name = "arg_enum_proc_macro"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "arrayvec"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
name = "as-slice"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516"
dependencies = [
"stable_deref_trait",
]
[[package]] [[package]]
name = "atk" name = "atk"
version = "0.18.2" version = "0.18.2"
@@ -153,6 +203,49 @@ version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
[[package]]
name = "av-scenechange"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394"
dependencies = [
"aligned",
"anyhow",
"arg_enum_proc_macro",
"arrayvec",
"log",
"num-rational",
"num-traits",
"pastey",
"rayon",
"thiserror 2.0.18",
"v_frame",
"y4m",
]
[[package]]
name = "av1-grain"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8"
dependencies = [
"anyhow",
"arrayvec",
"log",
"nom",
"num-rational",
"v_frame",
]
[[package]]
name = "avif-serialize"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7178fe5f7d460b13895ebb9dcb28a3a6216d2df2574a0806cb51b555d297f38"
dependencies = [
"arrayvec",
]
[[package]] [[package]]
name = "axum" name = "axum"
version = "0.8.9" version = "0.8.9"
@@ -233,6 +326,12 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
[[package]]
name = "bit_field"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "1.3.2" version = "1.3.2"
@@ -248,6 +347,15 @@ dependencies = [
"serde_core", "serde_core",
] ]
[[package]]
name = "bitstream-io"
version = "4.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7eff00be299a18769011411c9def0d827e8f2d7bf0c3dbf53633147a8867fd1f"
dependencies = [
"no_std_io2",
]
[[package]] [[package]]
name = "block-buffer" name = "block-buffer"
version = "0.10.4" version = "0.10.4"
@@ -296,6 +404,12 @@ dependencies = [
"tinyvec", "tinyvec",
] ]
[[package]]
name = "built"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c0e531d93d39c34eef561e929e8a7f86d77a5af08aac4f6d6e39976c51858e9"
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.20.3" version = "3.20.3"
@@ -403,6 +517,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dad887fd958be91b5098c0248def011f4523ab786cd411be668777e55063501f" checksum = "dad887fd958be91b5098c0248def011f4523ab786cd411be668777e55063501f"
dependencies = [ dependencies = [
"find-msvc-tools", "find-msvc-tools",
"jobserver",
"libc",
"shlex", "shlex",
] ]
@@ -473,6 +589,15 @@ dependencies = [
"strsim", "strsim",
] ]
[[package]]
name = "clap_complete"
version = "4.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0a7a9bfdb35811f9e59832f0f05975114d2251b415fb534108e6f34060fd772"
dependencies = [
"clap",
]
[[package]] [[package]]
name = "clap_derive" name = "clap_derive"
version = "4.6.1" version = "4.6.1"
@@ -500,6 +625,12 @@ dependencies = [
"error-code", "error-code",
] ]
[[package]]
name = "color_quant"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
[[package]] [[package]]
name = "colorchoice" name = "colorchoice"
version = "1.0.5" version = "1.0.5"
@@ -516,6 +647,19 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "console"
version = "0.15.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8"
dependencies = [
"encode_unicode",
"libc",
"once_cell",
"unicode-width",
"windows-sys 0.59.0",
]
[[package]] [[package]]
name = "cookie" name = "cookie"
version = "0.18.1" version = "0.18.1"
@@ -593,6 +737,25 @@ dependencies = [
"crossbeam-utils", "crossbeam-utils",
] ]
[[package]]
name = "crossbeam-deque"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
dependencies = [
"crossbeam-epoch",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
dependencies = [
"crossbeam-utils",
]
[[package]] [[package]]
name = "crossbeam-utils" name = "crossbeam-utils"
version = "0.8.21" version = "0.8.21"
@@ -878,6 +1041,12 @@ version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
[[package]]
name = "either"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]] [[package]]
name = "embed-resource" name = "embed-resource"
version = "3.0.9" version = "3.0.9"
@@ -898,6 +1067,12 @@ version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7"
[[package]]
name = "encode_unicode"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
[[package]] [[package]]
name = "encoding_rs" name = "encoding_rs"
version = "0.8.35" version = "0.8.35"
@@ -907,6 +1082,26 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "equator"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc"
dependencies = [
"equator-macro",
]
[[package]]
name = "equator-macro"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]] [[package]]
name = "equivalent" name = "equivalent"
version = "1.0.2" version = "1.0.2"
@@ -940,6 +1135,21 @@ version = "3.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59"
[[package]]
name = "exr"
version = "1.74.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be"
dependencies = [
"bit_field",
"half",
"lebe",
"miniz_oxide",
"rayon-core",
"smallvec",
"zune-inflate",
]
[[package]] [[package]]
name = "fastrand" name = "fastrand"
version = "2.4.1" version = "2.4.1"
@@ -1273,6 +1483,16 @@ dependencies = [
"wasip3", "wasip3",
] ]
[[package]]
name = "gif"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159"
dependencies = [
"color_quant",
"weezl",
]
[[package]] [[package]]
name = "gio" name = "gio"
version = "0.18.4" version = "0.18.4"
@@ -1727,10 +1947,17 @@ checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"
dependencies = [ dependencies = [
"bytemuck", "bytemuck",
"byteorder-lite", "byteorder-lite",
"color_quant",
"exr",
"gif",
"image-webp", "image-webp",
"moxcms", "moxcms",
"num-traits", "num-traits",
"png 0.18.1", "png 0.18.1",
"qoi",
"ravif",
"rayon",
"rgb",
"tiff", "tiff",
"zune-core", "zune-core",
"zune-jpeg", "zune-jpeg",
@@ -1746,6 +1973,12 @@ dependencies = [
"quick-error", "quick-error",
] ]
[[package]]
name = "imgref"
version = "1.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89194689a993ab15268672e99e7b0e19da2da3268ac682e8f02d29d4d1434cd7"
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "1.9.3" version = "1.9.3"
@@ -1769,6 +2002,19 @@ dependencies = [
"serde_core", "serde_core",
] ]
[[package]]
name = "indicatif"
version = "0.17.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235"
dependencies = [
"console",
"number_prefix",
"portable-atomic",
"unicode-width",
"web-time",
]
[[package]] [[package]]
name = "infer" name = "infer"
version = "0.19.0" version = "0.19.0"
@@ -1778,6 +2024,17 @@ dependencies = [
"cfb", "cfb",
] ]
[[package]]
name = "interpolate_name"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]] [[package]]
name = "ipnet" name = "ipnet"
version = "2.12.0" version = "2.12.0"
@@ -1790,6 +2047,15 @@ version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
name = "itertools"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
dependencies = [
"either",
]
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.18" version = "1.0.18"
@@ -1863,6 +2129,16 @@ dependencies = [
"syn 2.0.117", "syn 2.0.117",
] ]
[[package]]
name = "jobserver"
version = "0.1.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
dependencies = [
"getrandom 0.3.4",
"libc",
]
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.102" version = "0.3.102"
@@ -1913,6 +2189,12 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]]
name = "lebe"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8"
[[package]] [[package]]
name = "libappindicator" name = "libappindicator"
version = "0.9.0" version = "0.9.0"
@@ -1952,6 +2234,16 @@ dependencies = [
"pkg-config", "pkg-config",
] ]
[[package]]
name = "libfuzzer-sys"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9fd2f41a1cba099f79a0b6b6c35656cf7c03351a7bae8ff0f28f25270f929d2"
dependencies = [
"arbitrary",
"cc",
]
[[package]] [[package]]
name = "libloading" name = "libloading"
version = "0.7.4" version = "0.7.4"
@@ -1998,6 +2290,15 @@ version = "0.4.32"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a"
[[package]]
name = "loop9"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062"
dependencies = [
"imgref",
]
[[package]] [[package]]
name = "markup5ever" name = "markup5ever"
version = "0.38.0" version = "0.38.0"
@@ -2015,6 +2316,16 @@ version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
[[package]]
name = "maybe-rayon"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519"
dependencies = [
"cfg-if",
"rayon",
]
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.8.2" version = "2.8.2"
@@ -2135,6 +2446,15 @@ version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
[[package]]
name = "no_std_io2"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "418abd1b6d34fbf6cae440dc874771b0525a604428704c76e48b29a5e67b8003"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "nom" name = "nom"
version = "8.0.0" version = "8.0.0"
@@ -2144,12 +2464,59 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "noop_proc_macro"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
[[package]]
name = "num-bigint"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
dependencies = [
"num-integer",
"num-traits",
]
[[package]] [[package]]
name = "num-conv" name = "num-conv"
version = "0.2.2" version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
[[package]]
name = "num-derive"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "num-integer"
version = "0.1.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
dependencies = [
"num-traits",
]
[[package]]
name = "num-rational"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
dependencies = [
"num-bigint",
"num-integer",
"num-traits",
]
[[package]] [[package]]
name = "num-traits" name = "num-traits"
version = "0.2.19" version = "0.2.19"
@@ -2181,6 +2548,12 @@ dependencies = [
"syn 2.0.117", "syn 2.0.117",
] ]
[[package]]
name = "number_prefix"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
[[package]] [[package]]
name = "objc2" name = "objc2"
version = "0.6.4" version = "0.6.4"
@@ -2454,6 +2827,18 @@ dependencies = [
"windows-link 0.2.1", "windows-link 0.2.1",
] ]
[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "pastey"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec"
[[package]] [[package]]
name = "percent-encoding" name = "percent-encoding"
version = "2.3.2" version = "2.3.2"
@@ -2575,6 +2960,12 @@ dependencies = [
"miniz_oxide", "miniz_oxide",
] ]
[[package]]
name = "portable-atomic"
version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
[[package]] [[package]]
name = "potential_utf" name = "potential_utf"
version = "0.1.5" version = "0.1.5"
@@ -2590,6 +2981,15 @@ 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 = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "ppv-lite86"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [
"zerocopy",
]
[[package]] [[package]]
name = "precomputed-hash" name = "precomputed-hash"
version = "0.1.1" version = "0.1.1"
@@ -2668,12 +3068,40 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "profiling"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5"
dependencies = [
"profiling-procmacros",
]
[[package]]
name = "profiling-procmacros"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4488a4a36b9a4ba6b9334a32a39971f77c1436ec82c38707bce707699cc3bbcb"
dependencies = [
"quote",
"syn 2.0.117",
]
[[package]] [[package]]
name = "pxfm" name = "pxfm"
version = "0.1.29" version = "0.1.29"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f" checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f"
[[package]]
name = "qoi"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001"
dependencies = [
"bytemuck",
]
[[package]] [[package]]
name = "qr-core" name = "qr-core"
version = "0.1.0" version = "0.1.0"
@@ -2688,6 +3116,9 @@ version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap", "clap",
"clap_complete",
"image",
"indicatif",
"qr-core", "qr-core",
"serde", "serde",
"serde_json", "serde_json",
@@ -2756,12 +3187,111 @@ version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "rand"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
dependencies = [
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
dependencies = [
"getrandom 0.3.4",
]
[[package]]
name = "rav1e"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b"
dependencies = [
"aligned-vec",
"arbitrary",
"arg_enum_proc_macro",
"arrayvec",
"av-scenechange",
"av1-grain",
"bitstream-io",
"built",
"cfg-if",
"interpolate_name",
"itertools",
"libc",
"libfuzzer-sys",
"log",
"maybe-rayon",
"new_debug_unreachable",
"noop_proc_macro",
"num-derive",
"num-traits",
"paste",
"profiling",
"rand",
"rand_chacha",
"simd_helpers",
"thiserror 2.0.18",
"v_frame",
"wasm-bindgen",
]
[[package]]
name = "ravif"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45"
dependencies = [
"avif-serialize",
"imgref",
"loop9",
"quick-error",
"rav1e",
"rayon",
"rgb",
]
[[package]] [[package]]
name = "raw-window-handle" name = "raw-window-handle"
version = "0.6.2" 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 = "rayon"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"
dependencies = [
"either",
"rayon-core",
]
[[package]]
name = "rayon-core"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
dependencies = [
"crossbeam-deque",
"crossbeam-utils",
]
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.5.18" version = "0.5.18"
@@ -2889,6 +3419,12 @@ dependencies = [
"windows-sys 0.60.2", "windows-sys 0.60.2",
] ]
[[package]]
name = "rgb"
version = "0.8.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4"
[[package]] [[package]]
name = "rustc-hash" name = "rustc-hash"
version = "2.1.2" version = "2.1.2"
@@ -3238,6 +3774,15 @@ version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
[[package]]
name = "simd_helpers"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6"
dependencies = [
"quote",
]
[[package]] [[package]]
name = "siphasher" name = "siphasher"
version = "1.0.3" version = "1.0.3"
@@ -4242,6 +4787,12 @@ version = "1.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
[[package]]
name = "unicode-width"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
[[package]] [[package]]
name = "unicode-xid" name = "unicode-xid"
version = "0.2.6" version = "0.2.6"
@@ -4303,6 +4854,17 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "v_frame"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2"
dependencies = [
"aligned-vec",
"num-traits",
"wasm-bindgen",
]
[[package]] [[package]]
name = "version-compare" name = "version-compare"
version = "0.2.1" version = "0.2.1"
@@ -4560,6 +5122,16 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "web-time"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]] [[package]]
name = "web_atoms" name = "web_atoms"
version = "0.2.4" version = "0.2.4"
@@ -5317,6 +5889,12 @@ version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd"
[[package]]
name = "y4m"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448"
[[package]] [[package]]
name = "yoke" name = "yoke"
version = "0.8.3" version = "0.8.3"
@@ -5426,6 +6004,15 @@ version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9"
[[package]]
name = "zune-inflate"
version = "0.2.54"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02"
dependencies = [
"simd-adler32",
]
[[package]] [[package]]
name = "zune-jpeg" name = "zune-jpeg"
version = "0.5.15" version = "0.5.15"
+3
View File
@@ -12,6 +12,9 @@ path = "src/main.rs"
[dependencies] [dependencies]
qr-core = { path = "../core" } qr-core = { path = "../core" }
clap = { version = "4", features = ["derive"] } clap = { version = "4", features = ["derive"] }
clap_complete = "4"
anyhow = "1" anyhow = "1"
image = "0.25"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
indicatif = "0.17"
+231 -434
View File
@@ -1,513 +1,310 @@
use clap::Parser; use clap::{CommandFactory, Parser, Subcommand};
use clap_complete::{generate, Shell};
use indicatif::{ProgressBar, ProgressStyle};
use qr_core::qr::{QrCode, QrConfig, VersionMode}; use qr_core::qr::{QrCode, QrConfig, VersionMode};
use qr_core::text_builder; use qr_core::text_builder;
use qr_core::version::EcLevel; use qr_core::version::EcLevel;
use serde::Deserialize; use serde::Deserialize;
use std::fs; use std::io::{self, Read};
use std::path::Path; use std::path::Path;
use std::process;
// ──────────────────── 结构定义 ────────────────────
#[derive(Parser)] #[derive(Parser)]
#[command( #[command(
name = "qrgen", name = "qrgen",
about = "QR 码生成/解码工具 — 从零手搓的 ISO/IEC 18004 实现" version,
about = "QR 码生成/解码工具 — 从零手搓的 ISO/IEC 18004 实现",
after_help = "示例:\n qrgen encode \"Hello\" -o qr.png\n qrgen encode --mode wifi --ssid MyWiFi --password pass123\n qrgen decode qr.png\n echo \"Hello\" | qrgen encode -\n\n补全:\n qrgen --generate-completions bash > /usr/share/bash-completion/completions/qrgen"
)] )]
struct Args { struct Cli {
/// 快捷编码内容 #[command(subcommand)]
content: Option<String>, command: Command,
#[arg(long, value_name = "SHELL", value_parser = ["bash", "zsh", "fish", "powershell", "elvish"])]
generate_completions: Option<String>,
}
/// 解码图片文件 (PNG/JPEG/WebP) #[derive(Subcommand)]
#[arg(short = 'd', long)] #[allow(clippy::large_enum_variant)]
decode: Option<String>, enum Command {
/// 编码:文本 → QR 码
/// 输出文件 (.png 或 .svg),不指定则输出终端 ASCII Encode {
/// 要编码的内容(传 `-` 从 stdin 读取)
#[arg(default_value = "-")]
content: String,
/// 输出文件;不指定则终端 ASCII
#[arg(short = 'o', long)] #[arg(short = 'o', long)]
output: Option<String>, output: Option<String>,
#[command(flatten)]
opts: EncodeOpts,
},
/// 解码:QR 码图片 → 文本
Decode {
/// 图片文件路径(传 `-` 从 stdin 读取)
#[arg(default_value = "-")]
file: String,
},
}
/// 纠错级别 [L/M/Q/H] [default: M] #[derive(clap::Args, Clone)]
#[arg(short = 'l', long, default_value = "M")] struct EncodeOpts {
level: String, #[arg(short = 'l', long, default_value = "M")] level: String,
#[arg(short = 'V', long, value_parser = clap::value_parser!(u8).range(1..=40))] version: Option<u8>,
/// 手动指定版本 (1-40),不指定则自动选择 #[arg(short = 's', long, default_value = "4")] size: u8,
#[arg(short = 'v', long)] #[arg(short = 'm', long, default_value = "4")] margin: u8,
version: Option<u8>, #[arg(long)] fg: Option<String>,
#[arg(long)] bg: Option<String>,
/// 模块像素大小(仅 PNG[default: 4] #[arg(long)] logo: Option<String>,
#[arg(short = 's', long, default_value = "4")] #[arg(short = 'f', long, default_value = "png")] format: String,
size: u8, #[arg(long)] mode: Option<String>,
// WiFi
/// 白边模块数 [default: 4] #[arg(long)] ssid: Option<String>,
#[arg(short = 'm', long, default_value = "4")] #[arg(long)] password: Option<String>,
margin: u8, #[arg(long, default_value = "WPA")] encryption: String,
#[arg(long)] hidden: bool,
/// 反色(黑底白码) // vCard
#[arg(long)] #[arg(long)] name: Option<String>,
invert: bool, #[arg(long)] phone: Option<String>,
#[arg(long)] email: Option<String>,
/// 前景色 "#RRGGBB" #[arg(long)] company: Option<String>,
#[arg(long)] #[arg(long)] title: Option<String>,
fg: Option<String>, #[arg(long)] address: Option<String>,
#[arg(long = "vcard-url")] vcard_url: Option<String>,
/// 背景色 "#RRGGBB" #[arg(long)] birthday: Option<String>,
#[arg(long)] #[arg(long)] note: Option<String>,
bg: Option<String>, #[arg(long)] photo: Option<String>,
// Email
/// Logo 图片文件 #[arg(long)] to: Option<String>,
#[arg(long)] #[arg(long)] subject: Option<String>,
logo: Option<String>, #[arg(long)] body: Option<String>,
// Phone/SMS
/// 输出图像格式 [png/bmp/jpeg/webp] [default: png] #[arg(long)] number: Option<String>,
#[arg(short = 'f', long, default_value = "png")] #[arg(long)] message: Option<String>,
format: String, // URL
#[arg(long)] url: Option<String>,
// ---- 编码模式参数 ---- // Batch
/// 编码模式 [text/url/wifi/vcard/email/phone/sms/batch] #[arg(long)] batch: Option<String>,
#[arg(long)] #[arg(long)] output_dir: Option<String>,
mode: Option<String>,
/// WiFi SSID
#[arg(long)]
ssid: Option<String>,
/// WiFi 密码
#[arg(long)]
password: Option<String>,
/// WiFi 加密方式 [default: WPA]
#[arg(long, default_value = "WPA")]
encryption: String,
/// 隐藏 WiFi 网络
#[arg(long)]
hidden: bool,
/// 姓名 (vCard)
#[arg(long)]
name: Option<String>,
/// 电话 (vCard)
#[arg(long)]
phone: Option<String>,
/// 邮箱 (vCard)
#[arg(long)]
email: Option<String>,
/// 公司 (vCard)
#[arg(long)]
company: Option<String>,
/// 地址 (vCard)
#[arg(long)]
address: Option<String>,
/// 职位 (vCard)
#[arg(long)]
title: Option<String>,
/// 个人网址 (vCard)
#[arg(long = "vcard-url")]
vcard_url: Option<String>,
/// 生日 YYYY-MM-DD (vCard)
#[arg(long)]
birthday: Option<String>,
/// 备注 (vCard)
#[arg(long)]
note: Option<String>,
/// 照片 URL (vCard)
#[arg(long)]
photo: Option<String>,
/// 收件人 (Email)
#[arg(long)]
to: Option<String>,
/// 主题 (Email)
#[arg(long)]
subject: Option<String>,
/// 正文 (Email)
#[arg(long)]
body: Option<String>,
/// 电话号码 (Phone/SMS)
#[arg(long)]
number: Option<String>,
/// 短信内容 (SMS)
#[arg(long)]
message: Option<String>,
/// URL 链接
#[arg(long)]
url: Option<String>,
/// 批量输入文件 (JSON/CSV)
#[arg(long)]
batch: Option<String>,
/// 批量输出目录
#[arg(long)]
output_dir: Option<String>,
} }
#[derive(Deserialize)] #[derive(Deserialize)]
struct BatchEntry { struct BatchEntry {
content: Option<String>, content: Option<String>, level: Option<String>,
level: Option<String>, ssid: Option<String>, password: Option<String>, encryption: Option<String>,
ssid: Option<String>, #[serde(default)] hidden: Option<bool>,
password: Option<String>, name: Option<String>, phone: Option<String>, email: Option<String>,
encryption: Option<String>, company: Option<String>, address: Option<String>,
#[serde(default)] to: Option<String>, subject: Option<String>, body: Option<String>,
hidden: Option<bool>, number: Option<String>, message: Option<String>, url: Option<String>,
name: Option<String>,
phone: Option<String>,
email: Option<String>,
company: Option<String>,
address: Option<String>,
to: Option<String>,
subject: Option<String>,
body: Option<String>,
number: Option<String>,
message: Option<String>,
url: Option<String>,
} }
fn main() -> anyhow::Result<()> { // ──────────────────── 错误类型 ────────────────────
let args = Args::parse();
if let Some(path) = args.decode { struct E { msg: String, code: i32 }
return do_decode(&path); impl E {
fn new(m: impl Into<String>, c: i32) -> Self { Self { msg: m.into(), code: c } }
fn exit(&self) -> ! { eprintln!("qrgen: {}", self.msg); process::exit(self.code); }
}
impl From<std::io::Error> for E {
fn from(e: std::io::Error) -> Self { E::new(format!("IO 错误: {e}"), 2) }
}
impl From<image::ImageError> for E {
fn from(e: image::ImageError) -> Self { E::new(format!("图像错误: {e}"), 2) }
}
macro_rules! bail {
($msg:expr, $code:expr) => { return Err(E::new($msg, $code)) };
($fmt:expr, $code:expr, $($arg:tt)*) => { return Err(E::new(format!($fmt, $($arg)*), $code)) };
} }
if let Some(ref batch_file) = args.batch { // ──────────────────── 入口 ────────────────────
return do_batch(batch_file, &args);
fn main() {
let cli = Cli::parse();
if let Some(s) = cli.generate_completions {
if let Ok(sh) = s.parse::<Shell>() {
generate(sh, &mut Cli::command(), "qrgen", &mut io::stdout());
return;
}
}
let r = match cli.command {
Command::Encode { content, output, opts } => cmd_encode(&content, &output, &opts),
Command::Decode { file } => cmd_decode(&file),
};
if let Err(e) = r { e.exit(); }
} }
let text = build_text_from_args(&args)?; // ──────────────────── I/O 辅助 ────────────────────
let level = parse_level(&args.level)?;
let logo_bytes = args fn stdin_bytes() -> Result<Vec<u8>, E> {
.logo let mut b = Vec::new();
.as_ref() io::stdin().read_to_end(&mut b).map_err(|e| E::new(format!("无法读取 stdin: {e}"), 2))?;
.map(fs::read) Ok(b)
.transpose() }
.map_err(|e| anyhow::anyhow!("无法读取 logo 文件: {e}"))?; fn stdin_text() -> Result<String, E> {
let mut s = String::new();
io::stdin().read_to_string(&mut s).map_err(|e| E::new(format!("无法读取 stdin: {e}"), 2))?;
let t = s.trim().to_string();
if t.is_empty() { bail!("stdin 为空", 2); }
Ok(t)
}
// ──────────────────── 编码 ────────────────────
fn cmd_encode(content: &str, output: &Option<String>, opts: &EncodeOpts) -> Result<(), E> {
let text = if content == "-" { stdin_text()? } else { content.to_string() };
let final_text = if let Some(m) = &opts.mode {
build_mode(m, opts, &text)?
} else if let Some(ref bf) = opts.batch {
return do_batch(bf, opts);
} else { text };
let level = parse_level(&opts.level)?;
let logo = opts.logo.as_ref().map(std::fs::read).transpose()
.map_err(|e| E::new(format!("无法读取 logo: {e}"), 2))?;
let config = QrConfig { let config = QrConfig {
level, level, version: opts.version.map(VersionMode::Fixed).unwrap_or(VersionMode::Auto),
version: match args.version { margin: opts.margin, fg_color: opts.fg.clone(), bg_color: opts.bg.clone(),
Some(v) => {
if !(1..=40).contains(&v) {
anyhow::bail!("无效版本号: {}。支持 1-40", v);
}
VersionMode::Fixed(v)
}
None => VersionMode::Auto,
},
margin: args.margin,
fg_color: args.fg.clone(),
bg_color: args.bg.clone(),
}; };
let qr = QrCode::encode(&final_text, config).map_err(|e| E::new(format!("编码失败: {e}"), 1))?;
let qr = QrCode::encode(&text, config).map_err(|e| anyhow::anyhow!("编码失败: {}", e))?; match output {
Some(p) => {
match &args.output { check_path(p)?;
Some(path) => { let ext = Path::new(p).extension().and_then(|e| e.to_str()).unwrap_or("").to_lowercase();
check_path(path)?;
let ext = Path::new(path)
.extension()
.and_then(|e| e.to_str())
.unwrap_or("")
.to_lowercase();
match ext.as_str() { match ext.as_str() {
"svg" => { "svg" => { std::fs::write(p, qr.to_svg(logo.as_deref()))?; eprintln!("已生成: {p} (版本 {}, SVG)", qr.version.0); }
let svg = qr.to_svg(logo_bytes.as_deref());
fs::write(path, svg)?;
println!("已生成: {} (版本 {}, SVG 格式)", path, qr.version.0);
}
_ => { _ => {
let fmt = qr_core::render::image::OutputFormat::from_ext(&ext) let fmt = qr_core::render::image::OutputFormat::from_ext(&ext)
.or_else(|| qr_core::render::image::OutputFormat::from_ext(&args.format)) .or_else(|| qr_core::render::image::OutputFormat::from_ext(&opts.format))
.unwrap_or(qr_core::render::image::OutputFormat::Png); .unwrap_or(qr_core::render::image::OutputFormat::Png);
let bytes = qr.to_image_bytes(args.size, logo_bytes.as_deref(), Some(fmt))?; std::fs::write(p, qr.to_image_bytes(opts.size, logo.as_deref(), Some(fmt))?)?;
fs::write(path, bytes)?; eprintln!("已生成: {p} (版本 {}, {}×{}, {:?}, {})", qr.version.0, qr.size(), qr.size(), qr.level, fmt.extension());
println!(
"已生成: {} (版本 {}, {}×{} 模块, {:?} 级纠错, {})",
path,
qr.version.0,
qr.size(),
qr.size(),
qr.level,
fmt.extension()
);
} }
} }
} }
None => { None => { println!("{}", qr.to_ascii(false)); }
println!("{}", qr.to_ascii(args.invert));
} }
}
Ok(()) Ok(())
} }
fn build_text_from_args(args: &Args) -> anyhow::Result<String> { fn build_mode(mode: &str, opts: &EncodeOpts, fb: &str) -> Result<String, E> {
match args.mode.as_deref() { match mode {
Some("wifi") => { "wifi" => {
let ssid = args let s = opts.ssid.as_deref().ok_or_else(|| E::new("WiFi 模式需要 --ssid", 1))?;
.ssid Ok(text_builder::build_wifi_text(s, opts.password.as_deref().unwrap_or(""), &opts.encryption, opts.hidden))
.as_deref()
.ok_or_else(|| anyhow::anyhow!("WiFi 模式需要 --ssid"))?;
let pwd = args.password.as_deref().unwrap_or("");
Ok(text_builder::build_wifi_text(
ssid,
pwd,
&args.encryption,
args.hidden,
))
} }
Some("vcard") => Ok(text_builder::build_vcard_text( "vcard" => Ok(text_builder::build_vcard_text(
args.name.as_deref().unwrap_or(""), opts.name.as_deref().unwrap_or(""), opts.phone.as_deref().unwrap_or(""),
args.phone.as_deref().unwrap_or(""), opts.email.as_deref().unwrap_or(""), opts.company.as_deref().unwrap_or(""),
args.email.as_deref().unwrap_or(""), opts.address.as_deref().unwrap_or(""), opts.title.as_deref().unwrap_or(""),
args.company.as_deref().unwrap_or(""), opts.vcard_url.as_deref().unwrap_or(""), opts.birthday.as_deref().unwrap_or(""),
args.address.as_deref().unwrap_or(""), opts.note.as_deref().unwrap_or(""), opts.photo.as_deref().unwrap_or(""),
args.title.as_deref().unwrap_or(""),
args.vcard_url.as_deref().unwrap_or(""),
args.birthday.as_deref().unwrap_or(""),
args.note.as_deref().unwrap_or(""),
args.photo.as_deref().unwrap_or(""),
)), )),
Some("email") => { "email" => {
let to = args let t = opts.to.as_deref().ok_or_else(|| E::new("Email 模式需要 --to", 1))?;
.to Ok(text_builder::build_email_text(t, opts.subject.as_deref().unwrap_or(""), opts.body.as_deref().unwrap_or("")))
.as_deref()
.ok_or_else(|| anyhow::anyhow!("Email 模式需要 --to"))?;
Ok(text_builder::build_email_text(
to,
args.subject.as_deref().unwrap_or(""),
args.body.as_deref().unwrap_or(""),
))
} }
Some("phone") => { "phone" => Ok(text_builder::build_phone_text(opts.number.as_deref().ok_or_else(|| E::new("需要 --number", 1))?)),
let num = args "sms" => Ok(text_builder::build_sms_text(
.number opts.number.as_deref().ok_or_else(|| E::new("需要 --number", 1))?,
.as_deref() opts.message.as_deref().unwrap_or(""),
.ok_or_else(|| anyhow::anyhow!("电话模式需要 --number"))?; )),
Ok(text_builder::build_phone_text(num)) "url" => opts.url.clone().ok_or_else(|| E::new("URL 模式需要 --url", 1)),
} "text" => Ok(fb.to_string()),
Some("sms") => { _m => bail!("未知模式: {_m},支持 text/url/wifi/vcard/email/phone/sms/batch", 1),
let num = args
.number
.as_deref()
.ok_or_else(|| anyhow::anyhow!("短信模式需要 --number"))?;
Ok(text_builder::build_sms_text(
num,
args.message.as_deref().unwrap_or(""),
))
}
Some("url") => args
.url
.clone()
.ok_or_else(|| anyhow::anyhow!("URL 模式需要 --url")),
Some("text") => args
.content
.clone()
.ok_or_else(|| anyhow::anyhow!("文本模式需要提供编码内容")),
Some(m) => anyhow::bail!("未知模式: {m}。支持 text/url/wifi/vcard/email/phone/sms/batch"),
None => args
.content
.clone()
.ok_or_else(|| anyhow::anyhow!("请提供编码内容或使用 --mode 指定模式")),
} }
} }
fn do_decode(path: &str) -> anyhow::Result<()> { // ──────────────────── 解码 ────────────────────
let bytes = fs::read(path).map_err(|e| anyhow::anyhow!("无法读取文件 '{}': {}", path, e))?;
let result = qr_core::decoder::decode_image(&bytes).map_err(|e| anyhow::anyhow!("{e}"))?; fn cmd_decode(file: &str) -> Result<(), E> {
println!("解码成功:"); let bytes = if file == "-" { stdin_bytes()? } else { std::fs::read(file)? };
println!(" 文本: {}", result.text); let r = qr_core::decoder::decode_image(&bytes).map_err(|e| E::new(format!("解码失败: {e}"), 1))?;
println!(" 版本: {}", result.version); println!("{}", r.text);
println!(" 纠错级别: {:?}", result.level); eprintln!("版本: {} 级别: {:?} 掩码: {} 纠错: {} 码字", r.version, r.level, r.mask, r.errors_corrected);
println!(" 掩码: {}", result.mask);
if result.errors_corrected > 0 {
println!(" 纠正错误: {} 码字", result.errors_corrected);
}
Ok(()) Ok(())
} }
fn do_batch(file: &str, args: &Args) -> anyhow::Result<()> { // ──────────────────── 批量 ────────────────────
let input = fs::read_to_string(file)
.map_err(|e| anyhow::anyhow!("无法读取批量文件 '{}': {}", file, e))?;
fn do_batch(file: &str, opts: &EncodeOpts) -> Result<(), E> {
let input = std::fs::read_to_string(file)
.map_err(|e| E::new(format!("无法读取批量文件 '{file}': {e}"), 2))?;
let entries: Vec<BatchEntry> = serde_json::from_str(&input) let entries: Vec<BatchEntry> = serde_json::from_str(&input)
.or_else(|_| parse_csv(&input)) .or_else(|_| parse_csv(&input))
.map_err(|e| anyhow::anyhow!("无法解析输入: {e}\n支持 JSON 数组或 CSV 格式"))?; .map_err(|e| E::new(format!("解析失败: {e}"), 2))?;
let out = opts.output_dir.as_deref().unwrap_or("batch_output");
std::fs::create_dir_all(out).map_err(|e| E::new(format!("无法创建目录 '{out}': {e}"), 2))?;
let out_dir = args.output_dir.as_deref().unwrap_or("batch_output"); let total = entries.len();
fs::create_dir_all(out_dir)?; let pb = ProgressBar::new(total as u64);
pb.set_style(ProgressStyle::default_bar().template("{spinner:.green} [{bar:30.cyan/blue}] {pos}/{len} {msg}").unwrap());
for (i, entry) in entries.iter().enumerate() { for (i, e) in entries.iter().enumerate() {
let text = batch_entry_to_text(entry)?; let text = batch_text(e)?;
let level = entry let lvl = e.level.as_deref().map(parse_level).unwrap_or(Ok(EcLevel::M))?;
.level let cfg = QrConfig { level: lvl, version: VersionMode::Auto, margin: opts.margin, fg_color: opts.fg.clone(), bg_color: opts.bg.clone() };
.as_deref() let qr = QrCode::encode(&text, cfg).map_err(|e| E::new(e, 1))?;
.map(parse_level) let path = format!("{out}/qr_{:04}.png", i + 1);
.unwrap_or(Ok(EcLevel::M))?; std::fs::write(&path, qr.to_png_bytes(opts.size, None).map_err(|e| E::new(format!("{e}"), 2))?)?;
pb.set_message(path.clone());
let config = QrConfig { pb.inc(1);
level,
version: VersionMode::Auto,
margin: args.margin,
fg_color: args.fg.clone(),
bg_color: args.bg.clone(),
};
let qr = QrCode::encode(&text, config).map_err(|e| anyhow::anyhow!("{e}"))?;
let path = format!("{}/qr_{:04}.png", out_dir, i + 1);
let bytes = qr.to_png_bytes(args.size, None)?;
fs::write(&path, bytes)?;
println!("[{}/{}] {}", i + 1, entries.len(), path);
} }
pb.finish_with_message(format!("完成: {total} 个 QR → {out}"));
println!("批量生成完成: {} 个 QR 码 → {}", entries.len(), out_dir);
Ok(()) Ok(())
} }
fn batch_entry_to_text(entry: &BatchEntry) -> anyhow::Result<String> { fn batch_text(e: &BatchEntry) -> Result<String, E> {
if let Some(c) = &entry.content { if let Some(c) = &e.content { return Ok(c.clone()); }
return Ok(c.clone()); if let Some(u) = &e.url { return Ok(u.clone()); }
if let Some(s) = &e.ssid {
return Ok(text_builder::build_wifi_text(s, e.password.as_deref().unwrap_or(""), e.encryption.as_deref().unwrap_or("WPA"), e.hidden.unwrap_or(false)));
} }
if let Some(u) = &entry.url { if let Some(n) = &e.name {
return Ok(u.clone()); return Ok(text_builder::build_vcard_text(n, e.phone.as_deref().unwrap_or(""), e.email.as_deref().unwrap_or(""), e.company.as_deref().unwrap_or(""), e.address.as_deref().unwrap_or(""), "", "", "", "", ""));
} }
if let Some(s) = &entry.ssid { if let Some(t) = &e.to {
let p = entry.password.as_deref().unwrap_or(""); return Ok(text_builder::build_email_text(t, e.subject.as_deref().unwrap_or(""), e.body.as_deref().unwrap_or("")));
let e = entry.encryption.as_deref().unwrap_or("WPA");
let h = entry.hidden.unwrap_or(false);
return Ok(text_builder::build_wifi_text(s, p, e, h));
}
if let Some(n) = &entry.name {
let ph = entry.phone.as_deref().unwrap_or("");
let em = entry.email.as_deref().unwrap_or("");
let co = entry.company.as_deref().unwrap_or("");
let ad = entry.address.as_deref().unwrap_or("");
return Ok(text_builder::build_vcard_text(
n, ph, em, co, ad, "", "", "", "", "",
));
}
if let Some(t) = &entry.to {
let s = entry.subject.as_deref().unwrap_or("");
let b = entry.body.as_deref().unwrap_or("");
return Ok(text_builder::build_email_text(t, s, b));
}
if let Some(n) = &entry.number {
if let Some(m) = &entry.message {
return Ok(text_builder::build_sms_text(n, m));
} }
if let Some(n) = &e.number {
if let Some(m) = &e.message { return Ok(text_builder::build_sms_text(n, m)); }
return Ok(text_builder::build_phone_text(n)); return Ok(text_builder::build_phone_text(n));
} }
anyhow::bail!("无法识别的条目格式") bail!("无法识别的条目格式", 1)
} }
// ──────────────────── 工具函数 ────────────────────
fn parse_csv(input: &str) -> Result<Vec<BatchEntry>, String> { fn parse_csv(input: &str) -> Result<Vec<BatchEntry>, String> {
let mut lines = input.lines(); let mut lines = input.lines();
let header = lines.next().ok_or("CSV 为空")?; let cols: Vec<&str> = lines.next().ok_or("CSV 为空")?.split(',').map(|s| s.trim()).collect();
let columns: Vec<&str> = header.split(',').map(|s| s.trim()).collect(); let mut out = Vec::new();
let mut entries = Vec::new();
for line in lines { for line in lines {
if line.trim().is_empty() { if line.trim().is_empty() { continue; }
continue; let v: Vec<String> = line.split(',').map(|s| s.trim().trim_matches('"').to_string()).collect();
let (mut c,mut l,mut ss,mut pw,mut en,mut hi,mut na,mut ph,mut em,mut co,mut ad,mut to_,mut su,mut bo,mut nu,mut ms,mut ur) =
(None,None,None,None,None,None,None,None,None,None,None,None,None,None,None,None,None);
for (i, col) in cols.iter().enumerate() {
let val = v.get(i).cloned();
match *col { "content"=>c=val,"level"=>l=val,"ssid"=>ss=val,"password"=>pw=val,"encryption"=>en=val,"hidden"=>hi=val.map(|x|x=="true"),"name"=>na=val,"phone"=>ph=val,"email"=>em=val,"company"=>co=val,"address"=>ad=val,"to"=>to_=val,"subject"=>su=val,"body"=>bo=val,"number"=>nu=val,"message"=>ms=val,"url"=>ur=val, _=>{} }
} }
let values: Vec<String> = line out.push(BatchEntry{content:c,level:l,ssid:ss,password:pw,encryption:en,hidden:hi,name:na,phone:ph,email:em,company:co,address:ad,to:to_,subject:su,body:bo,number:nu,message:ms,url:ur});
.split(',')
.map(|s| s.trim().trim_matches('"').to_string())
.collect();
let mut content = None;
let mut level = None;
let mut ssid = None;
let mut password = None;
let mut encryption = None;
let mut hidden = None;
let mut name = None;
let mut phone = None;
let mut email = None;
let mut company = None;
let mut address = None;
let mut to = None;
let mut subject = None;
let mut body = None;
let mut number = None;
let mut message = None;
let mut url = None;
for (i, col) in columns.iter().enumerate() {
let val = values.get(i).cloned();
match *col {
"content" => content = val,
"level" => level = val,
"ssid" => ssid = val,
"password" => password = val,
"encryption" => encryption = val,
"hidden" => hidden = val.map(|v| v == "true"),
"name" => name = val,
"phone" => phone = val,
"email" => email = val,
"company" => company = val,
"address" => address = val,
"to" => to = val,
"subject" => subject = val,
"body" => body = val,
"number" => number = val,
"message" => message = val,
"url" => url = val,
_ => {}
} }
Ok(out)
} }
entries.push(BatchEntry { fn parse_level(s: &str) -> Result<EcLevel, E> {
content,
level,
ssid,
password,
encryption,
hidden,
name,
phone,
email,
company,
address,
to,
subject,
body,
number,
message,
url,
});
}
Ok(entries)
}
fn parse_level(s: &str) -> anyhow::Result<EcLevel> {
match s.to_uppercase().as_str() { match s.to_uppercase().as_str() {
"L" => Ok(EcLevel::L), "L"=>Ok(EcLevel::L),"M"=>Ok(EcLevel::M),"Q"=>Ok(EcLevel::Q),"H"=>Ok(EcLevel::H),
"M" => Ok(EcLevel::M), _ => bail!("无效纠错级别: '{s}',支持 L/M/Q/H", 1),
"Q" => Ok(EcLevel::Q),
"H" => Ok(EcLevel::H),
_ => anyhow::bail!("无效纠错级别: {}。支持 L/M/Q/H", s),
} }
} }
fn check_path(path: &str) -> anyhow::Result<()> { fn check_path(p: &str) -> Result<(), E> {
let path_obj = Path::new(path); if Path::new(p).components().any(|c| matches!(c, std::path::Component::ParentDir)) {
if path_obj bail!("路径不允许包含 '..'", 2);
.components()
.any(|c| matches!(c, std::path::Component::ParentDir))
{
anyhow::bail!("不允许包含 '..' 的路径,请使用当前目录下的文件名");
} }
Ok(()) Ok(())
} }