From e6a7efc7606df3cc371da3da0990426ee0226639 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E8=88=AA=E5=AE=87?= <3364451258@qq.com> Date: Sat, 20 Jun 2026 17:48:17 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20CLI=20P0-P2=20=E5=85=A8=E9=9D=A2?= =?UTF-8?q?=E6=94=B9=E8=BF=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- Cargo.lock | 587 ++++++++++++++++++++++++++++++++++++++++++ cli/Cargo.toml | 3 + cli/src/main.rs | 671 +++++++++++++++++------------------------------- 3 files changed, 824 insertions(+), 437 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e47ad7e..6786583 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,24 @@ dependencies = [ "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]] name = "alloc-no-stdlib" version = "2.0.4" @@ -97,6 +115,12 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + [[package]] name = "arboard" version = "3.6.1" @@ -118,6 +142,32 @@ dependencies = [ "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]] name = "atk" version = "0.18.2" @@ -153,6 +203,49 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "axum" version = "0.8.9" @@ -233,6 +326,12 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" +[[package]] +name = "bit_field" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" + [[package]] name = "bitflags" version = "1.3.2" @@ -248,6 +347,15 @@ dependencies = [ "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]] name = "block-buffer" version = "0.10.4" @@ -296,6 +404,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "built" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c0e531d93d39c34eef561e929e8a7f86d77a5af08aac4f6d6e39976c51858e9" + [[package]] name = "bumpalo" version = "3.20.3" @@ -403,6 +517,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dad887fd958be91b5098c0248def011f4523ab786cd411be668777e55063501f" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] @@ -473,6 +589,15 @@ dependencies = [ "strsim", ] +[[package]] +name = "clap_complete" +version = "4.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7a9bfdb35811f9e59832f0f05975114d2251b415fb534108e6f34060fd772" +dependencies = [ + "clap", +] + [[package]] name = "clap_derive" version = "4.6.1" @@ -500,6 +625,12 @@ dependencies = [ "error-code", ] +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "colorchoice" version = "1.0.5" @@ -516,6 +647,19 @@ dependencies = [ "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]] name = "cookie" version = "0.18.1" @@ -593,6 +737,25 @@ dependencies = [ "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]] name = "crossbeam-utils" version = "0.8.21" @@ -878,6 +1041,12 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + [[package]] name = "embed-resource" version = "3.0.9" @@ -898,6 +1067,12 @@ version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + [[package]] name = "encoding_rs" version = "0.8.35" @@ -907,6 +1082,26 @@ dependencies = [ "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]] name = "equivalent" version = "1.0.2" @@ -940,6 +1135,21 @@ version = "3.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "fastrand" version = "2.4.1" @@ -1273,6 +1483,16 @@ dependencies = [ "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]] name = "gio" version = "0.18.4" @@ -1727,10 +1947,17 @@ checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" dependencies = [ "bytemuck", "byteorder-lite", + "color_quant", + "exr", + "gif", "image-webp", "moxcms", "num-traits", "png 0.18.1", + "qoi", + "ravif", + "rayon", + "rgb", "tiff", "zune-core", "zune-jpeg", @@ -1746,6 +1973,12 @@ dependencies = [ "quick-error", ] +[[package]] +name = "imgref" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89194689a993ab15268672e99e7b0e19da2da3268ac682e8f02d29d4d1434cd7" + [[package]] name = "indexmap" version = "1.9.3" @@ -1769,6 +2002,19 @@ dependencies = [ "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]] name = "infer" version = "0.19.0" @@ -1778,6 +2024,17 @@ dependencies = [ "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]] name = "ipnet" version = "2.12.0" @@ -1790,6 +2047,15 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.18" @@ -1863,6 +2129,16 @@ dependencies = [ "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]] name = "js-sys" version = "0.3.102" @@ -1913,6 +2189,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" +[[package]] +name = "lebe" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" + [[package]] name = "libappindicator" version = "0.9.0" @@ -1952,6 +2234,16 @@ dependencies = [ "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]] name = "libloading" version = "0.7.4" @@ -1998,6 +2290,15 @@ version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", +] + [[package]] name = "markup5ever" version = "0.38.0" @@ -2015,6 +2316,16 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "memchr" version = "2.8.2" @@ -2135,6 +2446,15 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "nom" version = "8.0.0" @@ -2144,12 +2464,59 @@ dependencies = [ "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]] name = "num-conv" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "num-traits" version = "0.2.19" @@ -2181,6 +2548,12 @@ dependencies = [ "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]] name = "objc2" version = "0.6.4" @@ -2454,6 +2827,18 @@ dependencies = [ "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]] name = "percent-encoding" version = "2.3.2" @@ -2575,6 +2960,12 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + [[package]] name = "potential_utf" version = "0.1.5" @@ -2590,6 +2981,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "precomputed-hash" version = "0.1.1" @@ -2668,12 +3068,40 @@ dependencies = [ "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]] name = "pxfm" version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f" +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + [[package]] name = "qr-core" version = "0.1.0" @@ -2688,6 +3116,9 @@ version = "0.1.0" dependencies = [ "anyhow", "clap", + "clap_complete", + "image", + "indicatif", "qr-core", "serde", "serde_json", @@ -2756,12 +3187,111 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "raw-window-handle" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "redox_syscall" version = "0.5.18" @@ -2889,6 +3419,12 @@ dependencies = [ "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]] name = "rustc-hash" version = "2.1.2" @@ -3238,6 +3774,15 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "siphasher" version = "1.0.3" @@ -4242,6 +4787,12 @@ version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + [[package]] name = "unicode-xid" version = "0.2.6" @@ -4303,6 +4854,17 @@ dependencies = [ "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]] name = "version-compare" version = "0.2.1" @@ -4560,6 +5122,16 @@ dependencies = [ "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]] name = "web_atoms" version = "0.2.4" @@ -5317,6 +5889,12 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" +[[package]] +name = "y4m" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" + [[package]] name = "yoke" version = "0.8.3" @@ -5426,6 +6004,15 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "zune-jpeg" version = "0.5.15" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index c8ede61..9db6188 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -12,6 +12,9 @@ path = "src/main.rs" [dependencies] qr-core = { path = "../core" } clap = { version = "4", features = ["derive"] } +clap_complete = "4" anyhow = "1" +image = "0.25" serde = { version = "1", features = ["derive"] } serde_json = "1" +indicatif = "0.17" diff --git a/cli/src/main.rs b/cli/src/main.rs index 064dda4..f169424 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -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::text_builder; use qr_core::version::EcLevel; use serde::Deserialize; -use std::fs; +use std::io::{self, Read}; use std::path::Path; +use std::process; + +// ──────────────────── 结构定义 ──────────────────── #[derive(Parser)] #[command( 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 { - /// 快捷编码内容 - content: Option, +struct Cli { + #[command(subcommand)] + command: Command, + #[arg(long, value_name = "SHELL", value_parser = ["bash", "zsh", "fish", "powershell", "elvish"])] + generate_completions: Option, +} - /// 解码图片文件 (PNG/JPEG/WebP) - #[arg(short = 'd', long)] - decode: Option, +#[derive(Subcommand)] +#[allow(clippy::large_enum_variant)] +enum Command { + /// 编码:文本 → QR 码 + Encode { + /// 要编码的内容(传 `-` 从 stdin 读取) + #[arg(default_value = "-")] + content: String, + /// 输出文件;不指定则终端 ASCII + #[arg(short = 'o', long)] + output: Option, + #[command(flatten)] + opts: EncodeOpts, + }, + /// 解码:QR 码图片 → 文本 + Decode { + /// 图片文件路径(传 `-` 从 stdin 读取) + #[arg(default_value = "-")] + file: String, + }, +} - /// 输出文件 (.png 或 .svg),不指定则输出终端 ASCII - #[arg(short = 'o', long)] - output: Option, - - /// 纠错级别 [L/M/Q/H] [default: M] - #[arg(short = 'l', long, default_value = "M")] - level: String, - - /// 手动指定版本 (1-40),不指定则自动选择 - #[arg(short = 'v', long)] - version: Option, - - /// 模块像素大小(仅 PNG)[default: 4] - #[arg(short = 's', long, default_value = "4")] - size: u8, - - /// 白边模块数 [default: 4] - #[arg(short = 'm', long, default_value = "4")] - margin: u8, - - /// 反色(黑底白码) - #[arg(long)] - invert: bool, - - /// 前景色 "#RRGGBB" - #[arg(long)] - fg: Option, - - /// 背景色 "#RRGGBB" - #[arg(long)] - bg: Option, - - /// Logo 图片文件 - #[arg(long)] - logo: Option, - - /// 输出图像格式 [png/bmp/jpeg/webp] [default: png] - #[arg(short = 'f', long, default_value = "png")] - format: String, - - // ---- 编码模式参数 ---- - /// 编码模式 [text/url/wifi/vcard/email/phone/sms/batch] - #[arg(long)] - mode: Option, - - /// WiFi SSID - #[arg(long)] - ssid: Option, - - /// WiFi 密码 - #[arg(long)] - password: Option, - - /// WiFi 加密方式 [default: WPA] - #[arg(long, default_value = "WPA")] - encryption: String, - - /// 隐藏 WiFi 网络 - #[arg(long)] - hidden: bool, - - /// 姓名 (vCard) - #[arg(long)] - name: Option, - - /// 电话 (vCard) - #[arg(long)] - phone: Option, - - /// 邮箱 (vCard) - #[arg(long)] - email: Option, - - /// 公司 (vCard) - #[arg(long)] - company: Option, - - /// 地址 (vCard) - #[arg(long)] - address: Option, - - /// 职位 (vCard) - #[arg(long)] - title: Option, - - /// 个人网址 (vCard) - #[arg(long = "vcard-url")] - vcard_url: Option, - - /// 生日 YYYY-MM-DD (vCard) - #[arg(long)] - birthday: Option, - - /// 备注 (vCard) - #[arg(long)] - note: Option, - - /// 照片 URL (vCard) - #[arg(long)] - photo: Option, - - /// 收件人 (Email) - #[arg(long)] - to: Option, - - /// 主题 (Email) - #[arg(long)] - subject: Option, - - /// 正文 (Email) - #[arg(long)] - body: Option, - - /// 电话号码 (Phone/SMS) - #[arg(long)] - number: Option, - - /// 短信内容 (SMS) - #[arg(long)] - message: Option, - - /// URL 链接 - #[arg(long)] - url: Option, - - /// 批量输入文件 (JSON/CSV) - #[arg(long)] - batch: Option, - - /// 批量输出目录 - #[arg(long)] - output_dir: Option, +#[derive(clap::Args, Clone)] +struct EncodeOpts { + #[arg(short = 'l', long, default_value = "M")] level: String, + #[arg(short = 'V', long, value_parser = clap::value_parser!(u8).range(1..=40))] version: Option, + #[arg(short = 's', long, default_value = "4")] size: u8, + #[arg(short = 'm', long, default_value = "4")] margin: u8, + #[arg(long)] fg: Option, + #[arg(long)] bg: Option, + #[arg(long)] logo: Option, + #[arg(short = 'f', long, default_value = "png")] format: String, + #[arg(long)] mode: Option, + // WiFi + #[arg(long)] ssid: Option, + #[arg(long)] password: Option, + #[arg(long, default_value = "WPA")] encryption: String, + #[arg(long)] hidden: bool, + // vCard + #[arg(long)] name: Option, + #[arg(long)] phone: Option, + #[arg(long)] email: Option, + #[arg(long)] company: Option, + #[arg(long)] title: Option, + #[arg(long)] address: Option, + #[arg(long = "vcard-url")] vcard_url: Option, + #[arg(long)] birthday: Option, + #[arg(long)] note: Option, + #[arg(long)] photo: Option, + // Email + #[arg(long)] to: Option, + #[arg(long)] subject: Option, + #[arg(long)] body: Option, + // Phone/SMS + #[arg(long)] number: Option, + #[arg(long)] message: Option, + // URL + #[arg(long)] url: Option, + // Batch + #[arg(long)] batch: Option, + #[arg(long)] output_dir: Option, } #[derive(Deserialize)] struct BatchEntry { - content: Option, - level: Option, - ssid: Option, - password: Option, - encryption: Option, - #[serde(default)] - hidden: Option, - name: Option, - phone: Option, - email: Option, - company: Option, - address: Option, - to: Option, - subject: Option, - body: Option, - number: Option, - message: Option, - url: Option, + content: Option, level: Option, + ssid: Option, password: Option, encryption: Option, + #[serde(default)] hidden: Option, + name: Option, phone: Option, email: Option, + company: Option, address: Option, + to: Option, subject: Option, body: Option, + number: Option, message: Option, url: Option, } -fn main() -> anyhow::Result<()> { - let args = Args::parse(); +// ──────────────────── 错误类型 ──────────────────── - if let Some(path) = args.decode { - return do_decode(&path); +struct E { msg: String, code: i32 } +impl E { + fn new(m: impl Into, c: i32) -> Self { Self { msg: m.into(), code: c } } + fn exit(&self) -> ! { eprintln!("qrgen: {}", self.msg); process::exit(self.code); } +} +impl From for E { + fn from(e: std::io::Error) -> Self { E::new(format!("IO 错误: {e}"), 2) } +} +impl From 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)) }; +} + +// ──────────────────── 入口 ──────────────────── + +fn main() { + let cli = Cli::parse(); + if let Some(s) = cli.generate_completions { + if let Ok(sh) = s.parse::() { + 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(); } +} - if let Some(ref batch_file) = args.batch { - return do_batch(batch_file, &args); - } +// ──────────────────── I/O 辅助 ──────────────────── - let text = build_text_from_args(&args)?; - let level = parse_level(&args.level)?; - let logo_bytes = args - .logo - .as_ref() - .map(fs::read) - .transpose() - .map_err(|e| anyhow::anyhow!("无法读取 logo 文件: {e}"))?; +fn stdin_bytes() -> Result, E> { + let mut b = Vec::new(); + io::stdin().read_to_end(&mut b).map_err(|e| E::new(format!("无法读取 stdin: {e}"), 2))?; + Ok(b) +} +fn stdin_text() -> Result { + 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, 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 { - level, - version: match args.version { - 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(), + level, version: opts.version.map(VersionMode::Fixed).unwrap_or(VersionMode::Auto), + margin: opts.margin, fg_color: opts.fg.clone(), bg_color: opts.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 &args.output { - Some(path) => { - check_path(path)?; - let ext = Path::new(path) - .extension() - .and_then(|e| e.to_str()) - .unwrap_or("") - .to_lowercase(); - + match output { + Some(p) => { + check_path(p)?; + let ext = Path::new(p).extension().and_then(|e| e.to_str()).unwrap_or("").to_lowercase(); match ext.as_str() { - "svg" => { - let svg = qr.to_svg(logo_bytes.as_deref()); - fs::write(path, svg)?; - println!("已生成: {} (版本 {}, SVG 格式)", path, qr.version.0); - } + "svg" => { std::fs::write(p, qr.to_svg(logo.as_deref()))?; eprintln!("已生成: {p} (版本 {}, SVG)", qr.version.0); } _ => { 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); - let bytes = qr.to_image_bytes(args.size, logo_bytes.as_deref(), Some(fmt))?; - fs::write(path, bytes)?; - println!( - "已生成: {} (版本 {}, {}×{} 模块, {:?} 级纠错, {})", - path, - qr.version.0, - qr.size(), - qr.size(), - qr.level, - fmt.extension() - ); + std::fs::write(p, qr.to_image_bytes(opts.size, logo.as_deref(), Some(fmt))?)?; + eprintln!("已生成: {p} (版本 {}, {}×{}, {:?}, {})", qr.version.0, qr.size(), qr.size(), qr.level, fmt.extension()); } } } - None => { - println!("{}", qr.to_ascii(args.invert)); - } + None => { println!("{}", qr.to_ascii(false)); } } - Ok(()) } -fn build_text_from_args(args: &Args) -> anyhow::Result { - match args.mode.as_deref() { - Some("wifi") => { - let ssid = args - .ssid - .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, - )) +fn build_mode(mode: &str, opts: &EncodeOpts, fb: &str) -> Result { + match mode { + "wifi" => { + let s = opts.ssid.as_deref().ok_or_else(|| E::new("WiFi 模式需要 --ssid", 1))?; + Ok(text_builder::build_wifi_text(s, opts.password.as_deref().unwrap_or(""), &opts.encryption, opts.hidden)) } - Some("vcard") => Ok(text_builder::build_vcard_text( - args.name.as_deref().unwrap_or(""), - args.phone.as_deref().unwrap_or(""), - args.email.as_deref().unwrap_or(""), - args.company.as_deref().unwrap_or(""), - args.address.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(""), + "vcard" => Ok(text_builder::build_vcard_text( + opts.name.as_deref().unwrap_or(""), opts.phone.as_deref().unwrap_or(""), + opts.email.as_deref().unwrap_or(""), opts.company.as_deref().unwrap_or(""), + opts.address.as_deref().unwrap_or(""), opts.title.as_deref().unwrap_or(""), + opts.vcard_url.as_deref().unwrap_or(""), opts.birthday.as_deref().unwrap_or(""), + opts.note.as_deref().unwrap_or(""), opts.photo.as_deref().unwrap_or(""), )), - Some("email") => { - let to = args - .to - .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(""), - )) + "email" => { + let t = opts.to.as_deref().ok_or_else(|| E::new("Email 模式需要 --to", 1))?; + Ok(text_builder::build_email_text(t, opts.subject.as_deref().unwrap_or(""), opts.body.as_deref().unwrap_or(""))) } - Some("phone") => { - let num = args - .number - .as_deref() - .ok_or_else(|| anyhow::anyhow!("电话模式需要 --number"))?; - Ok(text_builder::build_phone_text(num)) - } - Some("sms") => { - 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 指定模式")), + "phone" => Ok(text_builder::build_phone_text(opts.number.as_deref().ok_or_else(|| E::new("需要 --number", 1))?)), + "sms" => Ok(text_builder::build_sms_text( + opts.number.as_deref().ok_or_else(|| E::new("需要 --number", 1))?, + opts.message.as_deref().unwrap_or(""), + )), + "url" => opts.url.clone().ok_or_else(|| E::new("URL 模式需要 --url", 1)), + "text" => Ok(fb.to_string()), + _m => bail!("未知模式: {_m},支持 text/url/wifi/vcard/email/phone/sms/batch", 1), } } -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}"))?; - println!("解码成功:"); - println!(" 文本: {}", result.text); - println!(" 版本: {}", result.version); - println!(" 纠错级别: {:?}", result.level); - println!(" 掩码: {}", result.mask); - if result.errors_corrected > 0 { - println!(" 纠正错误: {} 码字", result.errors_corrected); - } +// ──────────────────── 解码 ──────────────────── + +fn cmd_decode(file: &str) -> Result<(), E> { + let bytes = if file == "-" { stdin_bytes()? } else { std::fs::read(file)? }; + let r = qr_core::decoder::decode_image(&bytes).map_err(|e| E::new(format!("解码失败: {e}"), 1))?; + println!("{}", r.text); + eprintln!("版本: {} 级别: {:?} 掩码: {} 纠错: {} 码字", r.version, r.level, r.mask, r.errors_corrected); 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 = serde_json::from_str(&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"); - fs::create_dir_all(out_dir)?; + let total = entries.len(); + 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() { - let text = batch_entry_to_text(entry)?; - let level = entry - .level - .as_deref() - .map(parse_level) - .unwrap_or(Ok(EcLevel::M))?; - - let config = QrConfig { - 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); + for (i, e) in entries.iter().enumerate() { + let text = batch_text(e)?; + let lvl = e.level.as_deref().map(parse_level).unwrap_or(Ok(EcLevel::M))?; + let cfg = QrConfig { level: lvl, version: VersionMode::Auto, margin: opts.margin, fg_color: opts.fg.clone(), bg_color: opts.bg.clone() }; + let qr = QrCode::encode(&text, cfg).map_err(|e| E::new(e, 1))?; + let path = format!("{out}/qr_{:04}.png", i + 1); + std::fs::write(&path, qr.to_png_bytes(opts.size, None).map_err(|e| E::new(format!("{e}"), 2))?)?; + pb.set_message(path.clone()); + pb.inc(1); } - - println!("批量生成完成: {} 个 QR 码 → {}", entries.len(), out_dir); + pb.finish_with_message(format!("完成: {total} 个 QR → {out}")); Ok(()) } -fn batch_entry_to_text(entry: &BatchEntry) -> anyhow::Result { - if let Some(c) = &entry.content { - return Ok(c.clone()); +fn batch_text(e: &BatchEntry) -> Result { + if let Some(c) = &e.content { 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 { - return Ok(u.clone()); + if let Some(n) = &e.name { + 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 { - let p = entry.password.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(t) = &e.to { + return Ok(text_builder::build_email_text(t, e.subject.as_deref().unwrap_or(""), e.body.as_deref().unwrap_or(""))); } - 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)); } - anyhow::bail!("无法识别的条目格式") + bail!("无法识别的条目格式", 1) } +// ──────────────────── 工具函数 ──────────────────── + fn parse_csv(input: &str) -> Result, String> { let mut lines = input.lines(); - let header = lines.next().ok_or("CSV 为空")?; - let columns: Vec<&str> = header.split(',').map(|s| s.trim()).collect(); - let mut entries = Vec::new(); + let cols: Vec<&str> = lines.next().ok_or("CSV 为空")?.split(',').map(|s| s.trim()).collect(); + let mut out = Vec::new(); for line in lines { - if line.trim().is_empty() { - continue; + if line.trim().is_empty() { continue; } + let v: Vec = 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 = line - .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, - _ => {} - } - } - - entries.push(BatchEntry { - content, - level, - ssid, - password, - encryption, - hidden, - name, - phone, - email, - company, - address, - to, - subject, - body, - number, - message, - url, - }); + 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}); } - Ok(entries) + Ok(out) } -fn parse_level(s: &str) -> anyhow::Result { +fn parse_level(s: &str) -> Result { match s.to_uppercase().as_str() { - "L" => Ok(EcLevel::L), - "M" => Ok(EcLevel::M), - "Q" => Ok(EcLevel::Q), - "H" => Ok(EcLevel::H), - _ => anyhow::bail!("无效纠错级别: {}。支持 L/M/Q/H", s), + "L"=>Ok(EcLevel::L),"M"=>Ok(EcLevel::M),"Q"=>Ok(EcLevel::Q),"H"=>Ok(EcLevel::H), + _ => bail!("无效纠错级别: '{s}',支持 L/M/Q/H", 1), } } -fn check_path(path: &str) -> anyhow::Result<()> { - let path_obj = Path::new(path); - if path_obj - .components() - .any(|c| matches!(c, std::path::Component::ParentDir)) - { - anyhow::bail!("不允许包含 '..' 的路径,请使用当前目录下的文件名"); +fn check_path(p: &str) -> Result<(), E> { + if Path::new(p).components().any(|c| matches!(c, std::path::Component::ParentDir)) { + bail!("路径不允许包含 '..'", 2); } Ok(()) }