Files
QRGen/web/src/main.rs
T
Serendipity 38be82973e feat: Logo 嵌入 — QR 码中央叠加自定义图片
- PNG 渲染:RgbaImage 上使用 imageops::overlay 叠加 logo
- SVG 渲染:base64 编码 logo 嵌入 <image> 标签
- QrCode::to_png_bytes / to_svg 新增 Option<logo_bytes> 参数
- Logo 默认占 QR 区域 25%,建议配合 H 级纠错使用
2026-06-19 21:12:44 +08:00

137 lines
3.9 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use axum::{
extract::{Multipart, Query},
http::{header, StatusCode},
response::{Html, IntoResponse, Json},
routing::{get, post},
Router,
};
use qr_core::qr::{QrCode, QrConfig, VersionMode};
use qr_core::version::EcLevel;
use serde::{Deserialize, Serialize};
/// GET /api/qr 查询参数
#[derive(Deserialize)]
struct QrParams {
text: String,
#[serde(default = "default_level")]
level: String,
#[serde(default = "default_margin")]
margin: u8,
#[serde(default = "default_size")]
size: u8,
/// fmt=svg 返回 SVG,否则返回 PNG
#[serde(default)]
fmt: String,
}
fn default_level() -> String {
"M".into()
}
fn default_margin() -> u8 {
4
}
fn default_size() -> u8 {
8
}
fn parse_level(s: &str) -> Result<EcLevel, String> {
match s.to_uppercase().as_str() {
"L" => Ok(EcLevel::L),
"M" => Ok(EcLevel::M),
"Q" => Ok(EcLevel::Q),
"H" => Ok(EcLevel::H),
_ => Err(format!("无效纠错级别: {},支持 L/M/Q/H", s)),
}
}
/// 首页 HTML(编译期嵌入)
async fn index() -> Html<&'static str> {
Html(include_str!("templates/index.html"))
}
/// QR 码生成 API → PNG 或 SVG
async fn generate_qr(Query(params): Query<QrParams>) -> impl IntoResponse {
let level = match parse_level(&params.level) {
Ok(l) => l,
Err(e) => return (StatusCode::BAD_REQUEST, e).into_response(),
};
let config = QrConfig {
level,
version: VersionMode::Auto,
margin: params.margin,
..Default::default()
};
let qr = match QrCode::encode(&params.text, config) {
Ok(q) => q,
Err(e) => return (StatusCode::BAD_REQUEST, e).into_response(),
};
if params.fmt == "svg" {
let svg = qr.to_svg(None);
([(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(),
}
}
}
/// 解码结果 JSON 响应
#[derive(Serialize)]
struct DecodeResponse {
text: String,
version: u8,
level: String,
mask: u8,
errors_corrected: usize,
}
/// POST /api/decode — 解码上传的 QR 码图片
async fn decode_qr(mut multipart: Multipart) -> impl IntoResponse {
while let Ok(Some(field)) = multipart.next_field().await {
if field.name() == Some("file") {
match field.bytes().await {
Ok(data) => match qr_core::decoder::decode_image(&data) {
Ok(result) => {
return (
StatusCode::OK,
Json(DecodeResponse {
text: result.text,
version: result.version,
level: format!("{:?}", result.level),
mask: result.mask,
errors_corrected: result.errors_corrected,
}),
)
.into_response();
}
Err(e) => {
return (StatusCode::BAD_REQUEST, e).into_response();
}
},
Err(e) => {
return (StatusCode::BAD_REQUEST, e.to_string()).into_response();
}
}
}
}
(StatusCode::BAD_REQUEST, "未找到上传文件(字段名: file").into_response()
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/", get(index))
.route("/api/qr", get(generate_qr))
.route("/api/decode", post(decode_qr));
let addr = "0.0.0.0:3000";
println!("QRGen Web → http://{}", addr);
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
axum::serve(listener, app).await.unwrap();
}