Compare commits
8 Commits
ef6b092eda
...
v2.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 8298cd4c9c | |||
| e6a7efc760 | |||
| 5fb967a353 | |||
| 9a204d0312 | |||
| 171afab9bb | |||
| 7f3b8b4cc7 | |||
| 86d788e57c | |||
| b41f6ee7df |
+38
-1
@@ -1,5 +1,42 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.3.0 (2026-06-20)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **CLI 重构** — 子命令结构 `qrgen encode` / `qrgen decode`
|
||||||
|
- 符合 Rust 生态惯例(ripgrep/fd/cargo 风格)
|
||||||
|
- Shell 补全支持(`--generate-completions bash/zsh/fish/pwsh/elvish`)
|
||||||
|
- stdin 管道支持(`echo "text" | qrgen encode -`)
|
||||||
|
- 退出码规范化(0=成功, 1=输入错误, 2=系统错误)
|
||||||
|
- `-v` → `-V`(version),`-l` 保持不变
|
||||||
|
- 批量模式进度条(`indicatif` crate)
|
||||||
|
- 新增依赖:`clap_complete`、`indicatif`、`image`
|
||||||
|
|
||||||
|
## 0.3.0 (2026-06-19)
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **格式扩展** — 新增 BMP/JPEG/WebP 图像输出
|
||||||
|
- `core/src/render/image.rs`:`OutputFormat` 枚举 (Png/Bmp/Jpeg/WebP)
|
||||||
|
- `QrCode::to_image_bytes()` — 参数化格式输出
|
||||||
|
- CLI `-f`/`--format` (png/bmp/jpeg/webp)
|
||||||
|
- Web API `fmt` 参数扩展至全部 4 种格式
|
||||||
|
- **解码增强** — 透视矫正
|
||||||
|
- `core/src/decoder/perspective.rs`:旋转矫正流水线
|
||||||
|
- 自动检测 finder → 计算旋转角 → 仿射变换 → 再解码
|
||||||
|
- `decode_image` 自动重试矫正路径
|
||||||
|
- **vCard 扩展** — 新增 5 字段
|
||||||
|
- TITLE(职位)/ URL(网址)/ BDAY(生日)/ NOTE(备注)/ PHOTO(照片)
|
||||||
|
- Rust `text_builder` + TypeScript `qrText` + VCardMode UI 同步
|
||||||
|
- CLI 新增 `--title` `--vcard-url` `--birthday` `--note` `--photo`
|
||||||
|
- 中/英 i18n 翻译
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- `core/src/render/png.rs` → `image.rs`(格式无关化)
|
||||||
|
- `QrCode::to_png_bytes` 保留为 `to_image_bytes` 的便捷方法
|
||||||
|
|
||||||
## 0.1.0 (2026-06-19)
|
## 0.1.0 (2026-06-19)
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@@ -65,7 +102,7 @@
|
|||||||
- GUI:React Context + useReducer,共享文本构造工具 (utils/qrText.ts)
|
- GUI:React Context + useReducer,共享文本构造工具 (utils/qrText.ts)
|
||||||
- CLI:clap derive + anyhow 错误处理
|
- CLI:clap derive + anyhow 错误处理
|
||||||
- Web:axum 0.8 + tokio,编译期 HTML 嵌入 (include_str!)
|
- Web:axum 0.8 + tokio,编译期 HTML 嵌入 (include_str!)
|
||||||
- 96 个测试(72 单元 + 24 集成)
|
- 105 个测试(81 单元 + 24 集成)
|
||||||
- NSIS Windows 安装包 + Docker Alpine 镜像
|
- NSIS Windows 安装包 + Docker Alpine 镜像
|
||||||
- 文档:API doc comments(rustdoc 可用)+ 3 个代码示例
|
- 文档:API doc comments(rustdoc 可用)+ 3 个代码示例
|
||||||
- 社区:CONTRIBUTING / CODE_OF_CONDUCT / SECURITY / Issue & PR 模板
|
- 社区:CONTRIBUTING / CODE_OF_CONDUCT / SECURITY / Issue & PR 模板
|
||||||
|
|||||||
@@ -22,13 +22,13 @@ cargo build -p qr-core
|
|||||||
|
|
||||||
# CLI 构建
|
# CLI 构建
|
||||||
cargo build --release -p qrgen
|
cargo build --release -p qrgen
|
||||||
cargo run -p qrgen -- "Hello World"
|
cargo run -p qrgen -- encode "Hello World"
|
||||||
|
|
||||||
# Web 服务
|
# Web 服务
|
||||||
cargo run -p qrgen-web # → http://localhost:3000
|
cargo run -p qrgen-web # → http://localhost:3000
|
||||||
|
|
||||||
# CLI 解码
|
# CLI 解码
|
||||||
cargo run -p qrgen -- --decode test.png
|
cargo run -p qrgen -- decode test.png
|
||||||
|
|
||||||
# GUI 开发模式
|
# GUI 开发模式
|
||||||
cd gui/src-frontend && pnpm dev # 终端1: Vite 热更新
|
cd gui/src-frontend && pnpm dev # 终端1: Vite 热更新
|
||||||
@@ -89,6 +89,7 @@ QRGen/
|
|||||||
│ │ ├── rs_decode.rs # RS 纠错流水线
|
│ │ ├── rs_decode.rs # RS 纠错流水线
|
||||||
│ │ ├── mode_decode.rs # 逆向 4 种编码模式
|
│ │ ├── mode_decode.rs # 逆向 4 种编码模式
|
||||||
│ │ ├── detect.rs # 定位图案检测 + 采样网格
|
│ │ ├── detect.rs # 定位图案检测 + 采样网格
|
||||||
|
│ │ ├── perspective.rs # 透视矫正(旋转+仿射变换)
|
||||||
│ │ └── image.rs # 图像加载 + 二值化
|
│ │ └── image.rs # 图像加载 + 二值化
|
||||||
│ ├── matrix/
|
│ ├── matrix/
|
||||||
│ │ ├── grid.rs # 模块矩阵 (含 reserved 保留区)
|
│ │ ├── grid.rs # 模块矩阵 (含 reserved 保留区)
|
||||||
@@ -97,12 +98,13 @@ QRGen/
|
|||||||
│ │ ├── placement.rs # 蛇形数据排列
|
│ │ ├── placement.rs # 蛇形数据排列
|
||||||
│ │ └── mask.rs # 8 种掩码 + 四规则惩罚评分
|
│ │ └── mask.rs # 8 种掩码 + 四规则惩罚评分
|
||||||
│ └── render/
|
│ └── render/
|
||||||
│ ├── png.rs # PNG 输出 (image crate, 直接边界检测 margin)
|
│ ├── image.rs # 图像输出 (PNG/BMP/JPEG/WebP, image crate)
|
||||||
|
│ │ # OutputFormat 枚举,支持 Logo 叠加
|
||||||
│ ├── svg.rs # SVG 输出 (预分配容量)
|
│ ├── svg.rs # SVG 输出 (预分配容量)
|
||||||
│ └── ascii.rs # 终端 ASCII (██/ )
|
│ └── ascii.rs # 终端 ASCII (██/ )
|
||||||
├── cli/ # CLI 命令行 (依赖 core + clap + anyhow)
|
├── cli/ # CLI 命令行 (依赖 core + clap + clap_complete + indicatif)
|
||||||
│ └── src/main.rs # Args { content, -o, -l, -v, -s, -m, --invert }
|
│ └── src/main.rs # 子命令: encode/decode, stdin 管道, 批量进度条
|
||||||
│ # 含路径遍历防护(拒绝 .. 组件)
|
│ # 含路径遍历防护 + 退出码 + Shell 补全
|
||||||
├── gui/ # Tauri 桌面应用 (依赖 core + tauri-plugin-*)
|
├── gui/ # Tauri 桌面应用 (依赖 core + tauri-plugin-*)
|
||||||
│ ├── capabilities/default.json # ACL 权限 (store/dialog/clipboard/fs)
|
│ ├── capabilities/default.json # ACL 权限 (store/dialog/clipboard/fs)
|
||||||
│ ├── src/
|
│ ├── src/
|
||||||
@@ -145,7 +147,7 @@ QRGen/
|
|||||||
| Endpoint | 参数 | 返回 |
|
| Endpoint | 参数 | 返回 |
|
||||||
|----------|------|------|
|
|----------|------|------|
|
||||||
| `GET /` | — | HTML 页面(内嵌 7 种编码模式) |
|
| `GET /` | — | HTML 页面(内嵌 7 种编码模式) |
|
||||||
| `GET /api/qr` | `text`, `level`(L/M/Q/H), `margin`(1-20), `size`(2-20), `fmt`(svg) | PNG 或 SVG |
|
| `GET /api/qr` | `text`, `level`(L/M/Q/H), `margin`(1-20), `size`(2-20), `fmt`(png/bmp/jpeg/webp/svg) | PNG/BMP/JPEG/WebP/SVG |
|
||||||
| `POST /api/decode` | multipart `file` (PNG/JPEG/WebP) | JSON `{text, version, level, mask, errors_corrected}` |
|
| `POST /api/decode` | multipart `file` (PNG/JPEG/WebP) | JSON `{text, version, level, mask, errors_corrected}` |
|
||||||
|
|
||||||
## 前端状态管理
|
## 前端状态管理
|
||||||
@@ -196,9 +198,9 @@ Action: SET_MODE | SET_FORM_DATA | SET_CONFIG | SET_PREVIEW | SET_LOADING
|
|||||||
|
|
||||||
| 层级 | 数量 | 说明 |
|
| 层级 | 数量 | 说明 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| 单元测试 | 72 | Galois 运算、RS 编解码、模式编解码、掩码评分、格式/版本信息 roundtrip、BCH 容错、蛇形提取等 |
|
| 单元测试 | 81 | Galois 运算、RS 编解码、模式编解码、掩码评分、格式/版本信息 roundtrip、BCH 容错、蛇形提取、vCard 扩展等 |
|
||||||
| 集成测试 | 24 | 端到端编码、渲染输出验证、边距、特殊字符、自动版本选择、格式信息 roundtrip |
|
| 集成测试 | 24 | 端到端编码、渲染输出验证、边距、特殊字符、自动版本选择、格式信息 roundtrip |
|
||||||
| 总计 | 96 | `cargo test` 全部通过 |
|
| 总计 | 105 | `cargo test` 全部通过 |
|
||||||
|
|
||||||
## 版本号升级清单
|
## 版本号升级清单
|
||||||
|
|
||||||
|
|||||||
Generated
+587
@@ -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"
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<img src="https://img.shields.io/badge/axum-0.8-ff6b35" alt="axum">
|
<img src="https://img.shields.io/badge/axum-0.8-ff6b35" alt="axum">
|
||||||
<img src="https://img.shields.io/badge/docker-ready-2496ed" alt="docker">
|
<img src="https://img.shields.io/badge/docker-ready-2496ed" alt="docker">
|
||||||
<img src="https://img.shields.io/badge/license-MIT-green" alt="license">
|
<img src="https://img.shields.io/badge/license-MIT-green" alt="license">
|
||||||
<img src="https://img.shields.io/badge/tests-72%20passed-brightgreen" alt="tests">
|
<img src="https://img.shields.io/badge/tests-81%20passed-brightgreen" alt="tests">
|
||||||
<img src="https://img.shields.io/badge/clippy-clean-brightgreen" alt="clippy">
|
<img src="https://img.shields.io/badge/clippy-clean-brightgreen" alt="clippy">
|
||||||
<img src="https://img.shields.io/badge/prettier-formatted-ff69b4" alt="prettier">
|
<img src="https://img.shields.io/badge/prettier-formatted-ff69b4" alt="prettier">
|
||||||
<img src="https://img.shields.io/badge/eslint-checked-4b32c3" alt="eslint">
|
<img src="https://img.shields.io/badge/eslint-checked-4b32c3" alt="eslint">
|
||||||
@@ -128,21 +128,21 @@ sequenceDiagram
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 终端 ASCII 预览
|
# 终端 ASCII 预览
|
||||||
qrgen "Hello World"
|
qrgen encode "Hello World"
|
||||||
|
|
||||||
# 生成 PNG
|
# 生成 PNG
|
||||||
qrgen "https://example.com" -o qr.png -s 8
|
qrgen encode "https://example.com" -o qr.png -s 8
|
||||||
|
|
||||||
# 生成 SVG(高纠错)
|
# 生成 SVG(高纠错)
|
||||||
qrgen "重要数据" -o qr.svg -l H
|
qrgen encode "重要数据" -o qr.svg -l H
|
||||||
|
|
||||||
# 解码 QR 码图片
|
# 解码 QR 码图片
|
||||||
qrgen --decode qr.png
|
qrgen decode qr.png
|
||||||
```
|
```
|
||||||
|
|
||||||
### GUI 桌面应用
|
### GUI 桌面应用
|
||||||
|
|
||||||
- **7 种编码模式**:文本 / URL / WiFi / vCard / Email / 电话 / SMS
|
- **7 种编码模式**:文本 / URL / WiFi / vCard(10字段) / Email / 电话 / SMS
|
||||||
- **解码**:选择图片文件,解码 QR 码为文本
|
- **解码**:选择图片文件,解码 QR 码为文本
|
||||||
- **实时预览**:200ms 防抖,PNG 即时渲染
|
- **实时预览**:200ms 防抖,PNG 即时渲染
|
||||||
- **多格式导出**:PNG(可调模块大小)/ SVG / 复制到剪贴板
|
- **多格式导出**:PNG(可调模块大小)/ SVG / 复制到剪贴板
|
||||||
@@ -201,13 +201,13 @@ cd gui/src-frontend && pnpm dev # 终端1: 前端
|
|||||||
cargo run -p qrgen-gui # 终端2: Rust 后端
|
cargo run -p qrgen-gui # 终端2: Rust 后端
|
||||||
|
|
||||||
# CLI 开发
|
# CLI 开发
|
||||||
cargo run -p qrgen -- "Hello World"
|
cargo run -p qrgen -- encode "Hello World"
|
||||||
|
|
||||||
# Web 开发
|
# Web 开发
|
||||||
cargo run -p qrgen-web # → http://localhost:3000
|
cargo run -p qrgen-web # → http://localhost:3000
|
||||||
|
|
||||||
# Rust 测试
|
# Rust 测试
|
||||||
cargo test --lib # 72 unit
|
cargo test --lib # 81 unit
|
||||||
|
|
||||||
# 前端测试
|
# 前端测试
|
||||||
cd gui/src-frontend && pnpm test # vitest
|
cd gui/src-frontend && pnpm test # vitest
|
||||||
@@ -310,9 +310,10 @@ QRGen/
|
|||||||
| QR 版本 | 1 ~ 40(21×21 ~ 177×177 模块) |
|
| QR 版本 | 1 ~ 40(21×21 ~ 177×177 模块) |
|
||||||
| 纠错级别 | L (7%) / M (15%) / Q (25%) / H (30%) |
|
| 纠错级别 | L (7%) / M (15%) / Q (25%) / H (30%) |
|
||||||
| 编码模式 | 数字 / 字母数字 / 字节 / 汉字 (Shift JIS) |
|
| 编码模式 | 数字 / 字母数字 / 字节 / 汉字 (Shift JIS) |
|
||||||
| 输出格式 | PNG / SVG / 终端 ASCII |
|
| 输出格式 | PNG / BMP / JPEG / WebP / SVG / 终端 ASCII |
|
||||||
|
| vCard 字段 | 姓名/电话/邮箱/公司/职位/地址/网址/生日/备注/照片 (10 字段) |
|
||||||
| 使用方式 | Library / CLI / GUI / Web API |
|
| 使用方式 | Library / CLI / GUI / Web API |
|
||||||
| 解码 | 从图片识读 QR 码 → 文本(PNG/JPEG/WebP) |
|
| 解码 | 从图片识读 QR 码 → 文本(支持旋转矫正) |
|
||||||
| 自动版本选择 | 根据数据长度 + 纠错级别 |
|
| 自动版本选择 | 根据数据长度 + 纠错级别 |
|
||||||
| Docker 镜像 | ~18MB (alpine) |
|
| Docker 镜像 | ~18MB (alpine) |
|
||||||
|
|
||||||
@@ -358,6 +359,7 @@ cargo add qr-core
|
|||||||
|
|
||||||
## 社区
|
## 社区
|
||||||
|
|
||||||
|
- [CLI 使用手册](docs/CLI_USAGE.md) — 命令行完整指南
|
||||||
- [贡献指南](CONTRIBUTING.md) — 如何参与开发
|
- [贡献指南](CONTRIBUTING.md) — 如何参与开发
|
||||||
- [行为准则](CODE_OF_CONDUCT.md) — 社区规范
|
- [行为准则](CODE_OF_CONDUCT.md) — 社区规范
|
||||||
- [安全策略](SECURITY.md) — 漏洞报告流程
|
- [安全策略](SECURITY.md) — 漏洞报告流程
|
||||||
|
|||||||
+21
-18
@@ -2,34 +2,37 @@
|
|||||||
|
|
||||||
QRGen 的未来发展方向。
|
QRGen 的未来发展方向。
|
||||||
|
|
||||||
## v0.2.0 (下一个版本)
|
## v0.4.0 (下一个版本)
|
||||||
|
|
||||||
- [ ] **CLI 编码模式** — CLI 支持 `--mode wifi` 等子命令,免去手动拼 `WIFI:T:...`
|
|
||||||
- [ ] **Logo 嵌入** — QR 码中央嵌入自定义图片(Logo/头像)
|
|
||||||
- [ ] **彩色 QR 码** — 自定义前景色/背景色,渐变色支持
|
|
||||||
- [ ] **批量生成** — 从 CSV/JSON 批量生成 QR 码
|
|
||||||
- [ ] **前端测试** — vite + vitest + React Testing Library,80% 覆盖率
|
|
||||||
- [ ] **E2E 测试** — Playwright 端到端测试(编码 → 导出 → 历史)
|
- [ ] **E2E 测试** — Playwright 端到端测试(编码 → 导出 → 历史)
|
||||||
- [ ] **i18n** — 中英双语界面 (i18next)
|
- [ ] **解码增强 v2** — 完整透视变换(单应矩阵),模糊图像增强
|
||||||
|
- [ ] **PWA 支持** — Web 端可安装为 PWA,离线使用
|
||||||
## v0.3.0
|
- [ ] **跨平台 GUI** — macOS + Linux 桌面应用发布
|
||||||
|
|
||||||
- [ ] **格式扩展** — 支持 BMP/JPEG/WEBP 输出
|
|
||||||
- [ ] **解码增强** — 斜拍/旋转图像矫正、模糊图像增强
|
|
||||||
- [ ] **WiFi 扫码自动连接** — 移动端扫码后一键连接 WiFi
|
|
||||||
- [ ] **vCard 扩展** — 支持更多字段(照片、社交媒体等)
|
|
||||||
- [ ] **macOS 桌面应用** — Tauri macOS 构建支持
|
|
||||||
|
|
||||||
## v1.0.0 (长期)
|
## v1.0.0 (长期)
|
||||||
|
|
||||||
- [ ] **跨平台 GUI** — 完整的 Windows + macOS + Linux 桌面应用发布
|
|
||||||
- [ ] **PWA 支持** — Web 端可安装为 PWA,离线使用
|
|
||||||
- [ ] **发布到包管理器** — crates.io / winget / Homebrew / Scoop
|
- [ ] **发布到包管理器** — crates.io / winget / Homebrew / Scoop
|
||||||
|
- [ ] **WiFi 扫码自动连接** — 移动端扫码后一键连接 WiFi
|
||||||
- [ ] **插件系统** — 第三方编码模式扩展
|
- [ ] **插件系统** — 第三方编码模式扩展
|
||||||
- [ ] **在线服务** — 公开的 QR 码生成 API 服务(带速率限制)
|
- [ ] **在线服务** — 公开的 QR 码生成 API 服务(带速率限制)
|
||||||
|
|
||||||
## 已交付
|
## 已交付
|
||||||
|
|
||||||
|
### v0.3.0
|
||||||
|
|
||||||
|
- ✅ 格式扩展(BMP/JPEG/WebP 输出 + `OutputFormat` 枚举)
|
||||||
|
- ✅ 解码增强(旋转矫正 + 自动重试矫正流水线)
|
||||||
|
- ✅ vCard 扩展(10 字段:TITLE/URL/BDAY/NOTE/PHOTO)
|
||||||
|
|
||||||
|
### v0.2.0
|
||||||
|
|
||||||
|
- ✅ 彩色 QR 码(前景色/背景色 + PNG Rgba + SVG + CLI `--fg`/`--bg`)
|
||||||
|
- ✅ Logo 嵌入(PNG `imageops::overlay` + SVG base64)
|
||||||
|
- ✅ CLI 编码模式(`--mode wifi/vcard/email/phone/sms`)
|
||||||
|
- ✅ 批量生成(JSON/CSV 输入 → 自动编号输出)
|
||||||
|
- ✅ i18n 中英双语(i18next + react-i18next)
|
||||||
|
- ✅ 前端测试(19 tests,vitest + @vitest/coverage-v8)
|
||||||
|
|
||||||
### v0.1.0
|
### v0.1.0
|
||||||
|
|
||||||
- ✅ ISO/IEC 18004 完整 QR 码生成算法
|
- ✅ ISO/IEC 18004 完整 QR 码生成算法
|
||||||
@@ -40,7 +43,7 @@ QRGen 的未来发展方向。
|
|||||||
- ✅ Web 服务(axum + Docker alpine 17.7MB 镜像 + `/api/decode`)
|
- ✅ Web 服务(axum + Docker alpine 17.7MB 镜像 + `/api/decode`)
|
||||||
- ✅ QR 解码器(从零手写:定位→提取→RS纠错→模式解码,PNG/JPEG/WebP)
|
- ✅ QR 解码器(从零手写:定位→提取→RS纠错→模式解码,PNG/JPEG/WebP)
|
||||||
- ✅ RS 纠错解码(伴随式→Berlekamp-Massey→Chien→Forney)
|
- ✅ RS 纠错解码(伴随式→Berlekamp-Massey→Chien→Forney)
|
||||||
- ✅ 96 个 Rust 测试(72 单元 + 24 集成)
|
- ✅ 105 个 Rust 测试(81 单元 + 24 集成)
|
||||||
- ✅ 前端工程化(Prettier + ESLint + vitest + husky + commitlint)
|
- ✅ 前端工程化(Prettier + ESLint + vitest + husky + commitlint)
|
||||||
- ✅ crates.io 就绪(doc comments + 元数据 + 代码示例)
|
- ✅ crates.io 就绪(doc comments + 元数据 + 代码示例)
|
||||||
- ✅ 社区规范文件(CONTRIBUTING / CODE_OF_CONDUCT / SECURITY / ROADMAP / SUPPORT)
|
- ✅ 社区规范文件(CONTRIBUTING / CODE_OF_CONDUCT / SECURITY / ROADMAP / SUPPORT)
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
+235
-400
@@ -1,475 +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
|
||||||
// ---- 编码模式参数 ----
|
#[arg(long)] number: Option<String>,
|
||||||
/// 编码模式 [text/url/wifi/vcard/email/phone/sms/batch]
|
#[arg(long)] message: Option<String>,
|
||||||
#[arg(long)]
|
// URL
|
||||||
mode: Option<String>,
|
#[arg(long)] url: Option<String>,
|
||||||
|
// Batch
|
||||||
/// WiFi SSID
|
#[arg(long)] batch: Option<String>,
|
||||||
#[arg(long)]
|
#[arg(long)] output_dir: Option<String>,
|
||||||
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>,
|
|
||||||
|
|
||||||
/// 收件人 (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)) };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────── 入口 ────────────────────
|
||||||
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(ref batch_file) = args.batch {
|
|
||||||
return do_batch(batch_file, &args);
|
|
||||||
}
|
}
|
||||||
|
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() {
|
||||||
"png" => {
|
"svg" => { std::fs::write(p, qr.to_svg(logo.as_deref()))?; eprintln!("已生成: {p} (版本 {}, SVG)", qr.version.0); }
|
||||||
let bytes = qr.to_png_bytes(args.size, logo_bytes.as_deref())?;
|
_ => {
|
||||||
fs::write(path, bytes)?;
|
let fmt = qr_core::render::image::OutputFormat::from_ext(&ext)
|
||||||
println!(
|
.or_else(|| qr_core::render::image::OutputFormat::from_ext(&opts.format))
|
||||||
"已生成: {} (版本 {}, {}×{} 模块, {:?} 级纠错)",
|
.unwrap_or(qr_core::render::image::OutputFormat::Png);
|
||||||
path,
|
std::fs::write(p, qr.to_image_bytes(opts.size, logo.as_deref(), Some(fmt))?)?;
|
||||||
qr.version.0,
|
eprintln!("已生成: {p} (版本 {}, {}×{}, {:?}, {})", qr.version.0, qr.size(), qr.size(), qr.level, fmt.extension());
|
||||||
qr.size(),
|
|
||||||
qr.size(),
|
|
||||||
qr.level
|
|
||||||
);
|
|
||||||
}
|
|
||||||
"svg" => {
|
|
||||||
let svg = qr.to_svg(logo_bytes.as_deref());
|
|
||||||
fs::write(path, svg)?;
|
|
||||||
println!("已生成: {} (版本 {}, SVG 格式)", path, qr.version.0);
|
|
||||||
}
|
|
||||||
_ => anyhow::bail!("不支持的文件格式: .{}。支持 .png / .svg", ext),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => {
|
|
||||||
println!("{}", qr.to_ascii(args.invert));
|
|
||||||
}
|
}
|
||||||
|
None => { println!("{}", qr.to_ascii(false)); }
|
||||||
}
|
}
|
||||||
|
|
||||||
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(""),
|
||||||
)),
|
)),
|
||||||
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(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 {
|
|
||||||
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> {
|
fn parse_level(s: &str) -> Result<EcLevel, E> {
|
||||||
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(())
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -14,7 +14,7 @@ categories.workspace = true
|
|||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp"] }
|
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp", "bmp"] }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|||||||
+12
-1
@@ -19,6 +19,7 @@ mod extract;
|
|||||||
mod format;
|
mod format;
|
||||||
mod image;
|
mod image;
|
||||||
mod mode_decode;
|
mod mode_decode;
|
||||||
|
mod perspective;
|
||||||
mod rs_decode;
|
mod rs_decode;
|
||||||
|
|
||||||
use crate::matrix::mask::apply_mask;
|
use crate::matrix::mask::apply_mask;
|
||||||
@@ -48,7 +49,17 @@ pub struct DecodeResult {
|
|||||||
/// `DecodeResult` 包含解码文本和元信息
|
/// `DecodeResult` 包含解码文本和元信息
|
||||||
pub fn decode_image(bytes: &[u8]) -> Result<DecodeResult, String> {
|
pub fn decode_image(bytes: &[u8]) -> Result<DecodeResult, String> {
|
||||||
let gray = image::load_and_binarize(bytes)?;
|
let gray = image::load_and_binarize(bytes)?;
|
||||||
let detect_result = detect::detect_and_extract(&gray)?;
|
|
||||||
|
// 第一遍:直接检测
|
||||||
|
if let Ok(detect_result) = detect::detect_and_extract(&gray) {
|
||||||
|
if let Ok(result) = decode_matrix(&detect_result.modules) {
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 第二遍:尝试旋转矫正
|
||||||
|
let corrected = perspective::auto_correct(&gray);
|
||||||
|
let detect_result = detect::detect_and_extract(&corrected)?;
|
||||||
decode_matrix(&detect_result.modules)
|
decode_matrix(&detect_result.modules)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,160 @@
|
|||||||
|
//! QR 码图像透视矫正
|
||||||
|
//!
|
||||||
|
//! 检测定位图案后,计算旋转角并矫正图像。
|
||||||
|
//! MVP 版本:仅做旋转矫正(仿射变换),不做完整单应变换。
|
||||||
|
|
||||||
|
pub(crate) fn auto_correct(gray: &[Vec<bool>]) -> Vec<Vec<bool>> {
|
||||||
|
let h = gray.len();
|
||||||
|
let _w = if h > 0 {
|
||||||
|
gray[0].len()
|
||||||
|
} else {
|
||||||
|
return gray.to_vec();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 尝试找到至少 2 个 finder
|
||||||
|
let finders = find_two_finders(gray);
|
||||||
|
if finders.len() < 2 {
|
||||||
|
return gray.to_vec();
|
||||||
|
}
|
||||||
|
|
||||||
|
rotate_to_horizontal(gray, finders[0], finders[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 简化的 finder 检测(只找 2 个)
|
||||||
|
fn find_two_finders(gray: &[Vec<bool>]) -> Vec<(usize, usize)> {
|
||||||
|
let h = gray.len();
|
||||||
|
let _w = if h > 0 {
|
||||||
|
gray[0].len()
|
||||||
|
} else {
|
||||||
|
return vec![];
|
||||||
|
};
|
||||||
|
let mut centers: Vec<(usize, usize, usize)> = Vec::new(); // (cx, cy, size)
|
||||||
|
|
||||||
|
for row in (0..h).step_by(3) {
|
||||||
|
let runs = scan_row_runs(gray, row);
|
||||||
|
for i in 0..runs.len().saturating_sub(4) {
|
||||||
|
let avg = (runs[i].1 + runs[i + 1].1 + runs[i + 2].1 + runs[i + 3].1 + runs[i + 4].1)
|
||||||
|
as f32
|
||||||
|
/ 5.0;
|
||||||
|
if avg < 2.0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let r = [
|
||||||
|
runs[i].1 as f32,
|
||||||
|
runs[i + 1].1 as f32,
|
||||||
|
runs[i + 2].1 as f32,
|
||||||
|
runs[i + 3].1 as f32,
|
||||||
|
runs[i + 4].1 as f32,
|
||||||
|
];
|
||||||
|
let check = |v: f32, e: f32| (v - e * avg).abs() < avg * 0.4;
|
||||||
|
if check(r[0], 1.0)
|
||||||
|
&& check(r[1], 1.0)
|
||||||
|
&& check(r[2], 3.0)
|
||||||
|
&& check(r[3], 1.0)
|
||||||
|
&& check(r[4], 1.0)
|
||||||
|
{
|
||||||
|
let cx = runs[i + 2].0 + runs[i + 2].1 / 2;
|
||||||
|
let size =
|
||||||
|
runs[i].1 + runs[i + 1].1 + runs[i + 2].1 + runs[i + 3].1 + runs[i + 4].1;
|
||||||
|
centers.push((cx, row, size));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if centers.len() < 2 {
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按 X 坐标排序,取最左和最右
|
||||||
|
centers.sort_by_key(|c| c.0);
|
||||||
|
let left = centers.first().unwrap();
|
||||||
|
let right = centers.last().unwrap();
|
||||||
|
vec![(left.0, left.1), (right.0, right.1)]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scan_row_runs(gray: &[Vec<bool>], row: usize) -> Vec<(usize, usize)> {
|
||||||
|
let w = gray[0].len();
|
||||||
|
let mut runs = Vec::new();
|
||||||
|
let mut col = 0;
|
||||||
|
while col < w {
|
||||||
|
let current = gray[row][col];
|
||||||
|
let mut len = 0;
|
||||||
|
while col < w && gray[row][col] == current {
|
||||||
|
len += 1;
|
||||||
|
col += 1;
|
||||||
|
}
|
||||||
|
runs.push((col - len, len));
|
||||||
|
}
|
||||||
|
runs
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 旋转图像使 QR 码水平对齐
|
||||||
|
#[allow(clippy::needless_range_loop)]
|
||||||
|
fn rotate_to_horizontal(
|
||||||
|
gray: &[Vec<bool>],
|
||||||
|
tl: (usize, usize),
|
||||||
|
tr: (usize, usize),
|
||||||
|
) -> Vec<Vec<bool>> {
|
||||||
|
let h = gray.len();
|
||||||
|
let w = if h > 0 {
|
||||||
|
gray[0].len()
|
||||||
|
} else {
|
||||||
|
return gray.to_vec();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 计算旋转角(弧度)
|
||||||
|
let dx = tr.0 as f64 - tl.0 as f64;
|
||||||
|
let dy = tr.1 as f64 - tl.1 as f64;
|
||||||
|
let angle = dy.atan2(dx); // 正值 = 顺时针偏离水平
|
||||||
|
|
||||||
|
if angle.abs() < 0.01 {
|
||||||
|
// 已基本水平,不处理
|
||||||
|
return gray.to_vec();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 旋转中心 = 图像中心
|
||||||
|
let cx = w as f64 / 2.0;
|
||||||
|
let cy = h as f64 / 2.0;
|
||||||
|
let cos_a = angle.cos();
|
||||||
|
let sin_a = angle.sin();
|
||||||
|
|
||||||
|
// 计算旋转后尺寸
|
||||||
|
let corners = [
|
||||||
|
(0.0, 0.0),
|
||||||
|
(w as f64, 0.0),
|
||||||
|
(w as f64, h as f64),
|
||||||
|
(0.0, h as f64),
|
||||||
|
];
|
||||||
|
let (mut min_x, mut min_y, mut max_x, mut max_y) = (f64::MAX, f64::MAX, f64::MIN, f64::MIN);
|
||||||
|
for &(x, y) in &corners {
|
||||||
|
let rx = (x - cx) * cos_a - (y - cy) * sin_a + cx;
|
||||||
|
let ry = (x - cx) * sin_a + (y - cy) * cos_a + cy;
|
||||||
|
min_x = min_x.min(rx);
|
||||||
|
min_y = min_y.min(ry);
|
||||||
|
max_x = max_x.max(rx);
|
||||||
|
max_y = max_y.max(ry);
|
||||||
|
}
|
||||||
|
|
||||||
|
let new_w = (max_x - min_x).ceil() as usize;
|
||||||
|
let new_h = (max_y - min_y).ceil() as usize;
|
||||||
|
|
||||||
|
// 反向映射:对旋转后图像的每个像素,计算源图像中的位置,双线性插值
|
||||||
|
let mut result = vec![vec![false; new_w]; new_h];
|
||||||
|
|
||||||
|
for ny in 0..new_h {
|
||||||
|
for nx in 0..new_w {
|
||||||
|
// 映射回旋转前的坐标
|
||||||
|
let sx = (nx as f64 + min_x - cx) * cos_a + (ny as f64 + min_y - cy) * sin_a + cx;
|
||||||
|
let sy = -(nx as f64 + min_x - cx) * sin_a + (ny as f64 + min_y - cy) * cos_a + cy;
|
||||||
|
|
||||||
|
let sx_idx = sx as usize;
|
||||||
|
let sy_idx = sy as usize;
|
||||||
|
|
||||||
|
if sx_idx < w && sy_idx < h {
|
||||||
|
result[ny][nx] = gray[sy_idx][sx_idx];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
+22
-7
@@ -271,24 +271,39 @@ impl QrCode {
|
|||||||
crate::render::ascii::render_ascii(self, invert)
|
crate::render::ascii::render_ascii(self, invert)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 导出为 PNG 字节数据
|
/// 导出为图像字节数据(支持 PNG/BMP/JPEG/WebP)
|
||||||
///
|
///
|
||||||
/// `module_size` 控制每个模块的像素大小(2~20),越大文件越大。
|
/// `module_size` 控制每个模块的像素大小(2~20)。
|
||||||
/// `logo` 可选的 logo 图片字节,会在 QR 码中央叠加(建议搭配 H 级纠错)
|
/// `format` 输出格式,默认为 Png。
|
||||||
|
/// `logo` 可选的 logo 图片字节。
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// use qr_core::qr::{QrCode, QrConfig};
|
/// use qr_core::qr::{QrCode, QrConfig};
|
||||||
///
|
///
|
||||||
/// let qr = QrCode::encode("PNG test", QrConfig::default()).unwrap();
|
/// let qr = QrCode::encode("test", QrConfig::default()).unwrap();
|
||||||
/// let bytes = qr.to_png_bytes(4, None).unwrap();
|
/// let bytes = qr.to_image_bytes(4, None, None).unwrap();
|
||||||
/// std::fs::write("test.png", &bytes).unwrap();
|
|
||||||
/// ```
|
/// ```
|
||||||
|
pub fn to_image_bytes(
|
||||||
|
&self,
|
||||||
|
module_size: u8,
|
||||||
|
logo: Option<&[u8]>,
|
||||||
|
format: Option<crate::render::image::OutputFormat>,
|
||||||
|
) -> Result<Vec<u8>, image::ImageError> {
|
||||||
|
crate::render::image::render_image(
|
||||||
|
self,
|
||||||
|
module_size,
|
||||||
|
format.unwrap_or(crate::render::image::OutputFormat::Png),
|
||||||
|
logo,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 导出为 PNG 字节数据(便捷方法,兼容旧 API)
|
||||||
pub fn to_png_bytes(
|
pub fn to_png_bytes(
|
||||||
&self,
|
&self,
|
||||||
module_size: u8,
|
module_size: u8,
|
||||||
logo: Option<&[u8]>,
|
logo: Option<&[u8]>,
|
||||||
) -> Result<Vec<u8>, image::ImageError> {
|
) -> Result<Vec<u8>, image::ImageError> {
|
||||||
crate::render::png::render_png(self, module_size, logo)
|
self.to_image_bytes(module_size, logo, None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,62 @@
|
|||||||
|
//! QR 码图像渲染(支持 PNG/BMP/JPEG/WebP)
|
||||||
|
//!
|
||||||
|
//! 使用 `image` crate 将 QR 模块矩阵渲染为像素缓冲区,可选叠加 Logo。
|
||||||
|
|
||||||
use crate::qr::QrCode;
|
use crate::qr::QrCode;
|
||||||
use image::{imageops, ImageBuffer, Rgba, RgbaImage};
|
use image::{imageops, ImageBuffer, Rgba, RgbaImage};
|
||||||
|
|
||||||
|
/// 输出图像格式
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum OutputFormat {
|
||||||
|
Png,
|
||||||
|
Bmp,
|
||||||
|
Jpeg,
|
||||||
|
WebP,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OutputFormat {
|
||||||
|
/// 转为 `image` crate 的格式枚举
|
||||||
|
fn to_image_format(self) -> image::ImageFormat {
|
||||||
|
match self {
|
||||||
|
Self::Png => image::ImageFormat::Png,
|
||||||
|
Self::Bmp => image::ImageFormat::Bmp,
|
||||||
|
Self::Jpeg => image::ImageFormat::Jpeg,
|
||||||
|
Self::WebP => image::ImageFormat::WebP,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 文件扩展名(不含点)
|
||||||
|
pub fn extension(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Png => "png",
|
||||||
|
Self::Bmp => "bmp",
|
||||||
|
Self::Jpeg => "jpeg",
|
||||||
|
Self::WebP => "webp",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// MIME 类型
|
||||||
|
pub fn mime(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Png => "image/png",
|
||||||
|
Self::Bmp => "image/bmp",
|
||||||
|
Self::Jpeg => "image/jpeg",
|
||||||
|
Self::WebP => "image/webp",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 从扩展名解析
|
||||||
|
pub fn from_ext(ext: &str) -> Option<Self> {
|
||||||
|
match ext.to_lowercase().as_str() {
|
||||||
|
"png" => Some(Self::Png),
|
||||||
|
"bmp" => Some(Self::Bmp),
|
||||||
|
"jpeg" | "jpg" => Some(Self::Jpeg),
|
||||||
|
"webp" => Some(Self::WebP),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn fill_module(
|
fn fill_module(
|
||||||
img: &mut RgbaImage,
|
img: &mut RgbaImage,
|
||||||
x: u32,
|
x: u32,
|
||||||
@@ -24,32 +80,27 @@ fn fill_module(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 在 QR 码 PNG 缓冲区中央叠加 logo
|
|
||||||
fn overlay_logo(img: &mut RgbaImage, logo_bytes: &[u8], logo_size_pct: f32) -> Result<(), String> {
|
fn overlay_logo(img: &mut RgbaImage, logo_bytes: &[u8], logo_size_pct: f32) -> Result<(), String> {
|
||||||
let logo = image::load_from_memory(logo_bytes).map_err(|e| format!("Logo 加载失败: {e}"))?;
|
let logo = image::load_from_memory(logo_bytes).map_err(|e| format!("Logo 加载失败: {e}"))?;
|
||||||
let logo = logo.to_rgba8();
|
let logo = logo.to_rgba8();
|
||||||
|
|
||||||
let img_w = img.width();
|
let img_w = img.width();
|
||||||
let img_h = img.height();
|
let img_h = img.height();
|
||||||
|
|
||||||
// Logo 边长 = min(图像边长 * pct, 实际 QR 区域 * pct)
|
|
||||||
let logo_size = (img_w.min(img_h) as f32 * logo_size_pct) as u32;
|
let logo_size = (img_w.min(img_h) as f32 * logo_size_pct) as u32;
|
||||||
if logo_size < 4 {
|
if logo_size < 4 {
|
||||||
return Ok(()); // 太小,跳过
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let resized = imageops::resize(&logo, logo_size, logo_size, imageops::FilterType::Lanczos3);
|
let resized = imageops::resize(&logo, logo_size, logo_size, imageops::FilterType::Lanczos3);
|
||||||
|
|
||||||
let x = (img_w - logo_size) / 2;
|
let x = (img_w - logo_size) / 2;
|
||||||
let y = (img_h - logo_size) / 2;
|
let y = (img_h - logo_size) / 2;
|
||||||
imageops::overlay(img, &resized, x as i64, y as i64);
|
imageops::overlay(img, &resized, x as i64, y as i64);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render_png(
|
/// 渲染 QR 码到图像字节(支持 PNG/BMP/JPEG/WebP)
|
||||||
|
pub fn render_image(
|
||||||
qr: &QrCode,
|
qr: &QrCode,
|
||||||
module_size: u8,
|
module_size: u8,
|
||||||
|
format: OutputFormat,
|
||||||
logo: Option<&[u8]>,
|
logo: Option<&[u8]>,
|
||||||
) -> Result<Vec<u8>, image::ImageError> {
|
) -> Result<Vec<u8>, image::ImageError> {
|
||||||
let matrix_size = qr.size() as u32;
|
let matrix_size = qr.size() as u32;
|
||||||
@@ -72,7 +123,6 @@ pub fn render_png(
|
|||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
};
|
};
|
||||||
|
|
||||||
fill_module(
|
fill_module(
|
||||||
&mut img,
|
&mut img,
|
||||||
x,
|
x,
|
||||||
@@ -85,13 +135,14 @@ pub fn render_png(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Logo 叠加
|
|
||||||
if let Some(logo_data) = logo {
|
if let Some(logo_data) = logo {
|
||||||
// 忽略 logo 叠加错误(logo 有损不影响 QR 主体)
|
|
||||||
let _ = overlay_logo(&mut img, logo_data, 0.25);
|
let _ = overlay_logo(&mut img, logo_data, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut buf = Vec::new();
|
let mut buf = Vec::new();
|
||||||
img.write_to(&mut std::io::Cursor::new(&mut buf), image::ImageFormat::Png)?;
|
img.write_to(
|
||||||
|
&mut std::io::Cursor::new(&mut buf),
|
||||||
|
format.to_image_format(),
|
||||||
|
)?;
|
||||||
Ok(buf)
|
Ok(buf)
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
pub mod ascii;
|
pub mod ascii;
|
||||||
pub mod png;
|
pub mod image;
|
||||||
pub mod svg;
|
pub mod svg;
|
||||||
|
|||||||
@@ -8,15 +8,42 @@ pub fn build_wifi_text(ssid: &str, password: &str, encryption: &str, hidden: boo
|
|||||||
format!("WIFI:T:{encryption};S:{ssid};P:{password};{h};")
|
format!("WIFI:T:{encryption};S:{ssid};P:{password};{h};")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 构造 vCard 字符串
|
/// 构造 vCard 3.0 字符串(含扩展字段)
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn build_vcard_text(
|
pub fn build_vcard_text(
|
||||||
name: &str,
|
name: &str,
|
||||||
phone: &str,
|
phone: &str,
|
||||||
email: &str,
|
email: &str,
|
||||||
company: &str,
|
company: &str,
|
||||||
address: &str,
|
address: &str,
|
||||||
|
title: &str,
|
||||||
|
url: &str,
|
||||||
|
birthday: &str,
|
||||||
|
note: &str,
|
||||||
|
photo: &str,
|
||||||
) -> String {
|
) -> String {
|
||||||
format!("BEGIN:VCARD\nVERSION:3.0\nFN:{name}\nTEL:{phone}\nEMAIL:{email}\nORG:{company}\nADR:{address}\nEND:VCARD")
|
let mut s =
|
||||||
|
format!("BEGIN:VCARD\nVERSION:3.0\nFN:{name}\nTEL:{phone}\nEMAIL:{email}\nORG:{company}");
|
||||||
|
if !title.is_empty() {
|
||||||
|
s.push_str(&format!("\nTITLE:{title}"));
|
||||||
|
}
|
||||||
|
if !address.is_empty() {
|
||||||
|
s.push_str(&format!("\nADR:{address}"));
|
||||||
|
}
|
||||||
|
if !url.is_empty() {
|
||||||
|
s.push_str(&format!("\nURL:{url}"));
|
||||||
|
}
|
||||||
|
if !birthday.is_empty() {
|
||||||
|
s.push_str(&format!("\nBDAY:{birthday}"));
|
||||||
|
}
|
||||||
|
if !note.is_empty() {
|
||||||
|
s.push_str(&format!("\nNOTE:{note}"));
|
||||||
|
}
|
||||||
|
if !photo.is_empty() {
|
||||||
|
s.push_str(&format!("\nPHOTO:{photo}"));
|
||||||
|
}
|
||||||
|
s.push_str("\nEND:VCARD");
|
||||||
|
s
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 构造 mailto 链接
|
/// 构造 mailto 链接
|
||||||
@@ -71,10 +98,43 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_build_vcard_text() {
|
fn test_build_vcard_text() {
|
||||||
let text = build_vcard_text("张三", "13800138000", "a@b.com", "公司", "北京");
|
let text = build_vcard_text(
|
||||||
|
"张三",
|
||||||
|
"13800138000",
|
||||||
|
"a@b.com",
|
||||||
|
"公司",
|
||||||
|
"北京",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
);
|
||||||
assert!(text.contains("BEGIN:VCARD"));
|
assert!(text.contains("BEGIN:VCARD"));
|
||||||
assert!(text.contains("FN:张三"));
|
assert!(text.contains("FN:张三"));
|
||||||
assert!(text.contains("END:VCARD"));
|
assert!(text.contains("END:VCARD"));
|
||||||
|
assert!(!text.contains("TITLE:")); // 空字段不输出
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_build_vcard_full() {
|
||||||
|
let text = build_vcard_text(
|
||||||
|
"张三",
|
||||||
|
"13800138000",
|
||||||
|
"a@b.com",
|
||||||
|
"公司",
|
||||||
|
"北京",
|
||||||
|
"工程师",
|
||||||
|
"https://z.com",
|
||||||
|
"1990-01-01",
|
||||||
|
"备注",
|
||||||
|
"https://z.com/p.jpg",
|
||||||
|
);
|
||||||
|
assert!(text.contains("TITLE:工程师"));
|
||||||
|
assert!(text.contains("URL:https://z.com"));
|
||||||
|
assert!(text.contains("BDAY:1990-01-01"));
|
||||||
|
assert!(text.contains("NOTE:备注"));
|
||||||
|
assert!(text.contains("PHOTO:https://z.com/p.jpg"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -0,0 +1,397 @@
|
|||||||
|
# QRGen CLI 使用手册
|
||||||
|
|
||||||
|
`qrgen` 是 QRGen 的命令行工具,支持 QR 码的**编码**(文本 → QR 图)和**解码**(QR 图 → 文本),输出 PNG/BMP/JPEG/WebP/SVG/终端 ASCII 六种格式。
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 终端预览 QR 码
|
||||||
|
qrgen encode "Hello World"
|
||||||
|
|
||||||
|
# 生成 PNG
|
||||||
|
qrgen encode "https://example.com" -o qr.png
|
||||||
|
|
||||||
|
# 生成 SVG
|
||||||
|
qrgen encode "重要数据" -o qr.svg -l H
|
||||||
|
|
||||||
|
# 解码 QR 图片
|
||||||
|
qrgen decode qr.png
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 基础编码
|
||||||
|
|
||||||
|
### 语法
|
||||||
|
|
||||||
|
```bash
|
||||||
|
qrgen encode [OPTIONS] [CONTENT]
|
||||||
|
qrgen decode [FILE]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 选项
|
||||||
|
|
||||||
|
| 选项 | 说明 | 默认值 |
|
||||||
|
|------|------|--------|
|
||||||
|
| `-o, --output <文件>` | 输出文件路径(.png/.bmp/.jpg/.webp/.svg) | 终端 ASCII 输出 |
|
||||||
|
| `-l, --level <级别>` | 纠错级别:`L`(7%) `M`(15%) `Q`(25%) `H`(30%) | `M` |
|
||||||
|
| `-v, --version <1-40>` | 手动指定 QR 版本 | 自动选择 |
|
||||||
|
| `-s, --size <像素>` | 每个模块的像素大小(图像输出) | `4` |
|
||||||
|
| `-m, --margin <模块>` | 白边宽度(模块数) | `4` |
|
||||||
|
| `-f, --format <格式>` | 图像输出格式:`png` `bmp` `jpeg` `webp` | 从文件扩展名推断 |
|
||||||
|
| `--fg <颜色>` | 前景色 `#RRGGBB` 或 `#RGB` | `#000` |
|
||||||
|
| `--bg <颜色>` | 背景色 `#RRGGBB` 或 `#RGB` | `#FFF` |
|
||||||
|
| `--logo <文件>` | 在 QR 码中央叠加 Logo 图片 | 无 |
|
||||||
|
| `--invert` | 反色输出(终端 ASCII) | — |
|
||||||
|
|
||||||
|
### 示例
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 基础生成
|
||||||
|
qrgen encode "Hello World" -o hello.png
|
||||||
|
|
||||||
|
# 高纠错 + 大尺寸
|
||||||
|
qrgen encode "重要合同编号: ABC-12345678" -o contract.png -l H -s 8 -m 6
|
||||||
|
|
||||||
|
# 彩色 QR 码
|
||||||
|
qrgen encode "My Brand" -o brand.png --fg "#FF5733" --bg "#FFF8E7"
|
||||||
|
|
||||||
|
# 带 Logo
|
||||||
|
qrgen encode "https://mycompany.com" -o logo.png -l H --logo avatar.png
|
||||||
|
|
||||||
|
# JPEG 输出
|
||||||
|
qrgen encode "JPEG test" -o qr.jpg -f jpeg
|
||||||
|
|
||||||
|
# WebP 输出
|
||||||
|
qrgen encode "WebP test" -o qr.webp -f webp
|
||||||
|
|
||||||
|
# 指定版本(强制版本 5)
|
||||||
|
qrgen encode "Fixed version" -o fixed.png -v 5
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 编码模式
|
||||||
|
|
||||||
|
通过 `--mode` 参数可以使用 7 种编码模式,自动构造标准格式文本,无需手动拼接。
|
||||||
|
|
||||||
|
### 文本模式(默认)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
qrgen encode "任意文本内容" -o text.png
|
||||||
|
```
|
||||||
|
|
||||||
|
等同于:
|
||||||
|
```bash
|
||||||
|
qrgen encode --mode text "任意文本内容" -o text.png
|
||||||
|
```
|
||||||
|
|
||||||
|
### URL 模式
|
||||||
|
|
||||||
|
```bash
|
||||||
|
qrgen encode --mode url --url "https://github.com/LHY0125/QRGen" -o url.png
|
||||||
|
```
|
||||||
|
|
||||||
|
### WiFi 模式
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 基础 WiFi
|
||||||
|
qrgen encode --mode wifi --ssid MyWiFi --password pass123 -o wifi.png
|
||||||
|
|
||||||
|
# 无密码 WiFi
|
||||||
|
qrgen encode --mode wifi --ssid FreeWiFi --encryption nopass -o free-wifi.png
|
||||||
|
|
||||||
|
# 隐藏网络 + WPA2
|
||||||
|
qrgen encode --mode wifi --ssid HiddenNet --password secret --encryption WPA2 --hidden -o hidden.png
|
||||||
|
```
|
||||||
|
|
||||||
|
生成的 QR 内容格式:`WIFI:T:WPA;S:MyWiFi;P:pass123;;`
|
||||||
|
|
||||||
|
| 参数 | 说明 | 默认值 |
|
||||||
|
|------|------|--------|
|
||||||
|
| `--ssid` | WiFi 名称 | 必填 |
|
||||||
|
| `--password` | WiFi 密码 | 空 |
|
||||||
|
| `--encryption` | 加密方式:`WPA` `WPA2` `WEP` `nopass` | `WPA` |
|
||||||
|
| `--hidden` | 隐藏网络标志 | false |
|
||||||
|
|
||||||
|
### vCard 模式(电子名片)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 基础名片
|
||||||
|
qrgen encode --mode vcard \
|
||||||
|
--name "张三" \
|
||||||
|
--phone "13800138000" \
|
||||||
|
--email "zhangsan@example.com" \
|
||||||
|
-o card.png
|
||||||
|
|
||||||
|
# 完整名片(10 字段)
|
||||||
|
qrgen encode --mode vcard \
|
||||||
|
--name "张三" \
|
||||||
|
--phone "13800138000" \
|
||||||
|
--email "zhangsan@example.com" \
|
||||||
|
--company "科技有限公司" \
|
||||||
|
--title "高级工程师" \
|
||||||
|
--address "北京市海淀区" \
|
||||||
|
--vcard-url "https://zhangsan.com" \
|
||||||
|
--birthday "1995-06-15" \
|
||||||
|
--note "爱好: 编程, 摄影" \
|
||||||
|
--photo "https://zhangsan.com/avatar.jpg" \
|
||||||
|
-o full-card.png
|
||||||
|
```
|
||||||
|
|
||||||
|
| 参数 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `--name` | 姓名 |
|
||||||
|
| `--phone` | 电话 |
|
||||||
|
| `--email` | 邮箱 |
|
||||||
|
| `--company` | 公司 |
|
||||||
|
| `--title` | 职位 |
|
||||||
|
| `--address` | 地址 |
|
||||||
|
| `--vcard-url` | 个人网址 |
|
||||||
|
| `--birthday` | 生日 `YYYY-MM-DD` |
|
||||||
|
| `--note` | 备注 |
|
||||||
|
| `--photo` | 照片 URL |
|
||||||
|
|
||||||
|
### Email 模式
|
||||||
|
|
||||||
|
```bash
|
||||||
|
qrgen encode --mode email \
|
||||||
|
--to "hello@example.com" \
|
||||||
|
--subject "合作洽谈" \
|
||||||
|
--body "您好,我对贵公司的产品很感兴趣。" \
|
||||||
|
-o email.png
|
||||||
|
```
|
||||||
|
|
||||||
|
生成的 QR 内容格式:`mailto:hello@example.com?subject=...&body=...`
|
||||||
|
|
||||||
|
### 电话模式
|
||||||
|
|
||||||
|
```bash
|
||||||
|
qrgen encode --mode phone --number "13800138000" -o phone.png
|
||||||
|
```
|
||||||
|
|
||||||
|
扫描后直接拨号。
|
||||||
|
|
||||||
|
### 短信模式
|
||||||
|
|
||||||
|
```bash
|
||||||
|
qrgen encode --mode sms \
|
||||||
|
--number "13800138000" \
|
||||||
|
--message "你好,方便电话吗?" \
|
||||||
|
-o sms.png
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 批量生成
|
||||||
|
|
||||||
|
从 JSON 或 CSV 文件批量生成 QR 码。
|
||||||
|
|
||||||
|
### JSON 格式
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{"content": "产品 A", "level": "H"},
|
||||||
|
{"content": "产品 B", "level": "M"},
|
||||||
|
{"content": "产品 C"}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
qrgen encode --batch items.json --output-dir ./qr_output/
|
||||||
|
# 输出: qr_output/qr_0001.png, qr_output/qr_0002.png, ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### CSV 格式
|
||||||
|
|
||||||
|
```csv
|
||||||
|
content,level
|
||||||
|
"产品 A",H
|
||||||
|
"产品 B",M
|
||||||
|
"产品 C",M
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
qrgen encode --batch items.csv --output-dir ./qr_output/ -s 8
|
||||||
|
```
|
||||||
|
|
||||||
|
### 批量 WiFi 名片
|
||||||
|
|
||||||
|
JSON 中可以用模式专用字段:
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{"ssid": "Office-1F", "password": "pass1", "encryption": "WPA2"},
|
||||||
|
{"ssid": "Office-2F", "password": "pass2", "encryption": "WPA2"},
|
||||||
|
{"ssid": "Guest", "encryption": "nopass"}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
支持的批量字段:`content` `url` `ssid` `password` `encryption` `hidden` `name` `phone` `email` `company` `address` `to` `subject` `body` `number` `message`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 解码
|
||||||
|
|
||||||
|
从图片文件解码 QR 码内容。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
qrgen decode qr.png
|
||||||
|
qrgen -d qr.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
输出示例:
|
||||||
|
```
|
||||||
|
解码成功:
|
||||||
|
文本: https://example.com
|
||||||
|
版本: 3
|
||||||
|
纠错级别: M
|
||||||
|
掩码: 2
|
||||||
|
纠正错误: 0 码字
|
||||||
|
```
|
||||||
|
|
||||||
|
支持格式:PNG、JPEG、WebP、BMP。
|
||||||
|
|
||||||
|
解码器会自动对倾斜图片做旋转矫正。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 纠错级别说明
|
||||||
|
|
||||||
|
| 级别 | 纠错率 | 适用场景 |
|
||||||
|
|------|--------|----------|
|
||||||
|
| L | ~7% | 屏幕显示、数字传输 |
|
||||||
|
| M | ~15% | 日常打印(默认) |
|
||||||
|
| Q | ~25% | 可能被遮挡的场景 |
|
||||||
|
| H | ~30% | Logo 嵌入、户外使用 |
|
||||||
|
|
||||||
|
**注意:** 纠错越高,相同尺寸下可存储的文本越少。H 级建议与 Logo 嵌入配合使用。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 文件格式对比
|
||||||
|
|
||||||
|
| 格式 | 特点 | 适用场景 |
|
||||||
|
|------|------|----------|
|
||||||
|
| PNG | 无损压缩,支持透明 | 通用推荐 |
|
||||||
|
| SVG | 矢量,无限缩放 | 印刷、网页嵌入 |
|
||||||
|
| JPEG | 有损压缩,文件小 | 照片场景 |
|
||||||
|
| WebP | 现代格式,文件最小 | Web 优化 |
|
||||||
|
| BMP | 无压缩,文件大 | 嵌入式系统 |
|
||||||
|
| ASCII | 终端字符画 | 快速预览 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 完整参数列表
|
||||||
|
|
||||||
|
```
|
||||||
|
Usage: qrgen <COMMAND>
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
encode 编码:文本 → QR 码
|
||||||
|
decode 解码:QR 码图片 → 文本
|
||||||
|
|
||||||
|
通用选项:
|
||||||
|
--generate-completions <SHELL> 生成 Shell 补全脚本 (bash/zsh/fish/powershell/elvish)
|
||||||
|
-h, --help 显示帮助
|
||||||
|
-V, --version 显示版本
|
||||||
|
|
||||||
|
编码子命令 — qrgen encode [OPTIONS] [CONTENT]
|
||||||
|
-o, --output <FILE> 输出文件 (.png/.bmp/.jpg/.webp/.svg)
|
||||||
|
-l, --level <L/M/Q/H> 纠错级别 [default: M]
|
||||||
|
-V, --version <1-40> 手动指定版本 [default: auto]
|
||||||
|
-s, --size <PIXELS> 模块像素大小 [default: 4]
|
||||||
|
-m, --margin <MODULES> 白边模块数 [default: 4]
|
||||||
|
-f, --format <FORMAT> 图像格式: png/bmp/jpeg/webp [default: png]
|
||||||
|
--fg <#RRGGBB> 前景色 [default: #000]
|
||||||
|
--bg <#RRGGBB> 背景色 [default: #FFF]
|
||||||
|
--logo <FILE> Logo 图片文件
|
||||||
|
--mode <MODE> 编码模式: text/url/wifi/vcard/email/phone/sms/batch
|
||||||
|
--ssid <NAME> WiFi 名称
|
||||||
|
--password <PWD> WiFi 密码
|
||||||
|
--encryption <TYPE> 加密方式: WPA/WPA2/WEP/nopass [default: WPA]
|
||||||
|
--hidden 隐藏网络
|
||||||
|
--name <NAME> 姓名 (vCard)
|
||||||
|
--phone <NUMBER> 电话 (vCard)
|
||||||
|
--email <ADDR> 邮箱 (vCard)
|
||||||
|
--company <NAME> 公司 (vCard)
|
||||||
|
--title <TITLE> 职位 (vCard)
|
||||||
|
--address <ADDR> 地址 (vCard)
|
||||||
|
--vcard-url <URL> 个人网址 (vCard)
|
||||||
|
--birthday <DATE> 生日 YYYY-MM-DD (vCard)
|
||||||
|
--note <TEXT> 备注 (vCard)
|
||||||
|
--photo <URL> 照片 URL (vCard)
|
||||||
|
--to <ADDR> 收件人 (Email)
|
||||||
|
--subject <TEXT> 主题 (Email)
|
||||||
|
--body <TEXT> 正文 (Email)
|
||||||
|
--number <NUMBER> 电话号码 (Phone/SMS)
|
||||||
|
--message <TEXT> 短信内容 (SMS)
|
||||||
|
--url <URL> URL 链接
|
||||||
|
--batch <FILE> 批量输入文件 (JSON/CSV)
|
||||||
|
--output-dir <DIR> 批量输出目录
|
||||||
|
|
||||||
|
解码子命令 — qrgen decode [FILE]
|
||||||
|
[FILE] 图片文件路径(传 - 从 stdin 读取)
|
||||||
|
```
|
||||||
|
|
||||||
|
### stdin 管道
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 编码:从管道读入
|
||||||
|
echo "Hello" | qrgen encode - -o qr.png
|
||||||
|
cat long_text.txt | qrgen encode - -o qr.png
|
||||||
|
|
||||||
|
# 解码:从管道读入
|
||||||
|
curl -s https://api.example.com/qr.png | qrgen decode -
|
||||||
|
```
|
||||||
|
|
||||||
|
### Shell 补全
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 生成补全脚本
|
||||||
|
qrgen --generate-completions bash > /usr/share/bash-completion/completions/qrgen
|
||||||
|
qrgen --generate-completions zsh > /usr/share/zsh/site-functions/_qrgen
|
||||||
|
qrgen --generate-completions fish > ~/.config/fish/completions/qrgen.fish
|
||||||
|
```
|
||||||
|
|
||||||
|
### 退出码
|
||||||
|
|
||||||
|
| 码 | 含义 |
|
||||||
|
|----|------|
|
||||||
|
| 0 | 成功 |
|
||||||
|
| 1 | 输入错误(内容为空、模式不匹配、解码失败) |
|
||||||
|
| 2 | 系统错误(文件不存在、IO 错误) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
### Q: 生成的 QR 码扫不出来?
|
||||||
|
|
||||||
|
1. 确保静区足够:`-m 4` 或更大
|
||||||
|
2. 提高纠错级别:`-l H`
|
||||||
|
3. 增大模块尺寸:`-s 8` 或更高
|
||||||
|
4. 如果是彩色 QR,确保前景/背景对比度足够
|
||||||
|
|
||||||
|
### Q: 如何生成超大文本的 QR 码?
|
||||||
|
|
||||||
|
降低纠错级别(L 级容量最大),或让系统自动选择版本:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
qrgen encode "很长的文本..." -o big.png -l L
|
||||||
|
```
|
||||||
|
|
||||||
|
### Q: Logo 叠加后扫不出来?
|
||||||
|
|
||||||
|
1. 使用 H 级纠错(`-l H`)
|
||||||
|
2. Logo 尺寸占 QR 区域约 25%,系统会自动缩放
|
||||||
|
3. 确保 Logo 图片是 PNG 格式
|
||||||
|
|
||||||
|
### Q: 批量生成时如何指定纠错级别?
|
||||||
|
|
||||||
|
JSON 中每个条目可以设置 `"level": "H"`,也可以在命令行用 `-l H` 统一设置。
|
||||||
|
|
||||||
|
### Q: CSV 和 JSON 格式如何选择?
|
||||||
|
|
||||||
|
- JSON:支持嵌套字段,推荐用于 WiFi/vCard 等复杂模式
|
||||||
|
- CSV:简单表格,适合纯文本批量生成
|
||||||
@@ -47,7 +47,12 @@
|
|||||||
"phone": "Phone",
|
"phone": "Phone",
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
"company": "Company",
|
"company": "Company",
|
||||||
"address": "Address"
|
"title": "Title",
|
||||||
|
"address": "Address",
|
||||||
|
"url": "URL",
|
||||||
|
"birthday": "Birthday",
|
||||||
|
"note": "Note",
|
||||||
|
"photo": "Photo URL"
|
||||||
},
|
},
|
||||||
"email": {
|
"email": {
|
||||||
"to": "To",
|
"to": "To",
|
||||||
|
|||||||
@@ -47,7 +47,12 @@
|
|||||||
"phone": "电话",
|
"phone": "电话",
|
||||||
"email": "邮箱",
|
"email": "邮箱",
|
||||||
"company": "公司",
|
"company": "公司",
|
||||||
"address": "地址"
|
"title": "职位",
|
||||||
|
"address": "地址",
|
||||||
|
"url": "网址",
|
||||||
|
"birthday": "生日",
|
||||||
|
"note": "备注",
|
||||||
|
"photo": "照片URL"
|
||||||
},
|
},
|
||||||
"email": {
|
"email": {
|
||||||
"to": "收件人",
|
"to": "收件人",
|
||||||
|
|||||||
@@ -8,7 +8,12 @@ const FIELDS = [
|
|||||||
{ key: 'phone', i18n: 'vcard.phone' },
|
{ key: 'phone', i18n: 'vcard.phone' },
|
||||||
{ key: 'email', i18n: 'vcard.email' },
|
{ key: 'email', i18n: 'vcard.email' },
|
||||||
{ key: 'company', i18n: 'vcard.company' },
|
{ key: 'company', i18n: 'vcard.company' },
|
||||||
|
{ key: 'title', i18n: 'vcard.title' },
|
||||||
{ key: 'address', i18n: 'vcard.address' },
|
{ key: 'address', i18n: 'vcard.address' },
|
||||||
|
{ key: 'vcardUrl', i18n: 'vcard.url' },
|
||||||
|
{ key: 'birthday', i18n: 'vcard.birthday' },
|
||||||
|
{ key: 'note', i18n: 'vcard.note' },
|
||||||
|
{ key: 'photo', i18n: 'vcard.photo' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function VCardMode() {
|
export default function VCardMode() {
|
||||||
|
|||||||
@@ -14,14 +14,26 @@ export function buildWifiText(formData: Record<string, string>): string {
|
|||||||
return `WIFI:T:${encryption};S:${ssid};P:${password};${hidden};`;
|
return `WIFI:T:${encryption};S:${ssid};P:${password};${hidden};`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 构造 vCard 字符串 */
|
/** 构造 vCard 3.0 字符串(含扩展字段) */
|
||||||
export function buildVCardText(formData: Record<string, string>): string {
|
export function buildVCardText(formData: Record<string, string>): string {
|
||||||
const name = formData.name || '';
|
const name = formData.name || '';
|
||||||
const phone = formData.phone || '';
|
const phone = formData.phone || '';
|
||||||
const email = formData.email || '';
|
const email = formData.email || '';
|
||||||
const company = formData.company || '';
|
const company = formData.company || '';
|
||||||
const address = formData.address || '';
|
const address = formData.address || '';
|
||||||
return `BEGIN:VCARD\nVERSION:3.0\nFN:${name}\nTEL:${phone}\nEMAIL:${email}\nORG:${company}\nADR:${address}\nEND:VCARD`;
|
const title = formData.title || '';
|
||||||
|
const url = formData.vcardUrl || '';
|
||||||
|
const birthday = formData.birthday || '';
|
||||||
|
const note = formData.note || '';
|
||||||
|
const photo = formData.photo || '';
|
||||||
|
let s = `BEGIN:VCARD\nVERSION:3.0\nFN:${name}\nTEL:${phone}\nEMAIL:${email}\nORG:${company}`;
|
||||||
|
if (title) s += `\nTITLE:${title}`;
|
||||||
|
if (address) s += `\nADR:${address}`;
|
||||||
|
if (url) s += `\nURL:${url}`;
|
||||||
|
if (birthday) s += `\nBDAY:${birthday}`;
|
||||||
|
if (note) s += `\nNOTE:${note}`;
|
||||||
|
if (photo) s += `\nPHOTO:${photo}`;
|
||||||
|
return s + '\nEND:VCARD';
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 构造 mailto 链接 */
|
/** 构造 mailto 链接 */
|
||||||
|
|||||||
+6
-5
@@ -70,12 +70,13 @@ async fn generate_qr(Query(params): Query<QrParams>) -> impl IntoResponse {
|
|||||||
|
|
||||||
if params.fmt == "svg" {
|
if params.fmt == "svg" {
|
||||||
let svg = qr.to_svg(None);
|
let svg = qr.to_svg(None);
|
||||||
([(header::CONTENT_TYPE, "image/svg+xml")], svg).into_response()
|
return ([(header::CONTENT_TYPE, "image/svg+xml")], svg).into_response();
|
||||||
} else {
|
|
||||||
match qr.to_png_bytes(params.size, None) {
|
|
||||||
Ok(b) => ([(header::CONTENT_TYPE, "image/png")], b).into_response(),
|
|
||||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
|
||||||
}
|
}
|
||||||
|
let fmt = qr_core::render::image::OutputFormat::from_ext(¶ms.fmt)
|
||||||
|
.unwrap_or(qr_core::render::image::OutputFormat::Png);
|
||||||
|
match qr.to_image_bytes(params.size, None, Some(fmt)) {
|
||||||
|
Ok(b) => ([(header::CONTENT_TYPE, fmt.mime())], b).into_response(),
|
||||||
|
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user