docs(infra): 补充中文注释
This commit is contained in:
parent
2db652137d
commit
671a9fea7a
@ -1,10 +1,17 @@
|
|||||||
|
//! 前端全局上下文定义。
|
||||||
|
//!
|
||||||
|
//! 当前保存当前登录用户的信息,供 Dioxus 组件在客户端与服务端渲染期间共享访问。
|
||||||
|
|
||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::models::user::PublicUser;
|
use crate::models::user::PublicUser;
|
||||||
|
|
||||||
|
/// 用户上下文,用于在组件树中传递登录状态。
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
pub struct UserContext {
|
pub struct UserContext {
|
||||||
|
/// 当前登录用户,未登录时为 `None`。
|
||||||
pub user: Signal<Option<Arc<PublicUser>>>,
|
pub user: Signal<Option<Arc<PublicUser>>>,
|
||||||
|
/// 是否已完成会话校验,避免重复触发验证请求。
|
||||||
pub checked: Signal<bool>,
|
pub checked: Signal<bool>,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,8 @@
|
|||||||
|
//! 语法高亮模块。
|
||||||
|
//!
|
||||||
|
//! 仅在 `server` feature 启用时可用,使用 `syntect` 将代码块转换为带 CSS class 的 HTML,
|
||||||
|
//! 配合 `public/highlight.css` 中生成的主题规则实现亮/暗主题高亮。
|
||||||
|
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
pub mod server {
|
pub mod server {
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
@ -6,6 +11,7 @@ pub mod server {
|
|||||||
use syntect::parsing::SyntaxSet;
|
use syntect::parsing::SyntaxSet;
|
||||||
use syntect::util::LinesWithEndings;
|
use syntect::util::LinesWithEndings;
|
||||||
|
|
||||||
|
/// 全局语法集合,懒加载时合并内置语法与 `syntaxes/` 目录下的自定义语法。
|
||||||
static SYNTAX_SET: LazyLock<SyntaxSet> = LazyLock::new(|| {
|
static SYNTAX_SET: LazyLock<SyntaxSet> = LazyLock::new(|| {
|
||||||
let mut builder = SyntaxSet::load_defaults_newlines().into_builder();
|
let mut builder = SyntaxSet::load_defaults_newlines().into_builder();
|
||||||
if let Err(e) = builder.add_from_folder("syntaxes/", true) {
|
if let Err(e) = builder.add_from_folder("syntaxes/", true) {
|
||||||
@ -14,16 +20,23 @@ pub mod server {
|
|||||||
builder.build()
|
builder.build()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// 根据语言标识查找对应的语法定义。
|
||||||
|
///
|
||||||
|
/// 依次尝试:扩展名、语法名称、小写扩展名/名称、常用别名映射。
|
||||||
|
/// 如果全部失败,则回退到纯文本语法。
|
||||||
fn find_syntax(lang: Option<&str>) -> &'static syntect::parsing::SyntaxReference {
|
fn find_syntax(lang: Option<&str>) -> &'static syntect::parsing::SyntaxReference {
|
||||||
let ss = &*SYNTAX_SET;
|
let ss = &*SYNTAX_SET;
|
||||||
if let Some(lang) = lang {
|
if let Some(lang) = lang {
|
||||||
if !lang.is_empty() {
|
if !lang.is_empty() {
|
||||||
|
// 尝试按扩展名匹配
|
||||||
if let Some(s) = ss.find_syntax_by_extension(lang) {
|
if let Some(s) = ss.find_syntax_by_extension(lang) {
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
// 尝试按语法名称匹配
|
||||||
if let Some(s) = ss.find_syntax_by_name(lang) {
|
if let Some(s) = ss.find_syntax_by_name(lang) {
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
// 小写后再匹配一次
|
||||||
let lower = lang.to_lowercase();
|
let lower = lang.to_lowercase();
|
||||||
if lower != lang {
|
if lower != lang {
|
||||||
if let Some(s) = ss.find_syntax_by_extension(&lower) {
|
if let Some(s) = ss.find_syntax_by_extension(&lower) {
|
||||||
@ -33,6 +46,7 @@ pub mod server {
|
|||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 常用语言别名映射表
|
||||||
let aliases: &[(&str, &str)] = &[
|
let aliases: &[(&str, &str)] = &[
|
||||||
("rust", "rs"),
|
("rust", "rs"),
|
||||||
("js", "js"),
|
("js", "js"),
|
||||||
@ -68,6 +82,9 @@ pub mod server {
|
|||||||
.expect("no plain text syntax")
|
.expect("no plain text syntax")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 对给定代码字符串按指定语言进行高亮,返回 HTML 字符串。
|
||||||
|
///
|
||||||
|
/// 输出使用 spaced CSS class 风格,便于与 `highlight.css` 中的选择器匹配。
|
||||||
pub fn highlight_code(code: &str, lang: Option<&str>) -> String {
|
pub fn highlight_code(code: &str, lang: Option<&str>) -> String {
|
||||||
let trimmed = code.trim();
|
let trimmed = code.trim();
|
||||||
let syntax = find_syntax(lang);
|
let syntax = find_syntax(lang);
|
||||||
@ -75,6 +92,7 @@ pub mod server {
|
|||||||
let mut generator =
|
let mut generator =
|
||||||
ClassedHTMLGenerator::new_with_class_style(syntax, ss, ClassStyle::Spaced);
|
ClassedHTMLGenerator::new_with_class_style(syntax, ss, ClassStyle::Spaced);
|
||||||
|
|
||||||
|
// 逐行解析,出错时记录警告并继续
|
||||||
for line in LinesWithEndings::from(trimmed) {
|
for line in LinesWithEndings::from(trimmed) {
|
||||||
if let Err(e) = generator.parse_html_for_line_which_includes_newline(line) {
|
if let Err(e) = generator.parse_html_for_line_which_includes_newline(line) {
|
||||||
tracing::warn!("syntect parse error: {:?}", e);
|
tracing::warn!("syntect parse error: {:?}", e);
|
||||||
|
|||||||
@ -1,15 +1,22 @@
|
|||||||
|
//! IP 与用户代理信息定期清理后台任务。
|
||||||
|
//!
|
||||||
|
//! 仅在 `server` feature 启用时编译,每天运行一次。
|
||||||
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use tokio::time::interval;
|
use tokio::time::interval;
|
||||||
|
|
||||||
use crate::db::pool::get_conn;
|
use crate::db::pool::get_conn;
|
||||||
|
|
||||||
|
/// 启动 IP 信息清理循环,每天将 90 天前的评论的 `ip_address` 与 `user_agent` 置空。
|
||||||
pub async fn run_purge() {
|
pub async fn run_purge() {
|
||||||
|
// 每天触发一次
|
||||||
let mut ticker = interval(Duration::from_secs(86400));
|
let mut ticker = interval(Duration::from_secs(86400));
|
||||||
loop {
|
loop {
|
||||||
ticker.tick().await;
|
ticker.tick().await;
|
||||||
match get_conn().await {
|
match get_conn().await {
|
||||||
Ok(client) => {
|
Ok(client) => {
|
||||||
|
// 仅清理 90 天前且仍保留 IP 的评论
|
||||||
if let Err(e) = client
|
if let Err(e) = client
|
||||||
.execute("UPDATE comments SET ip_address = NULL, user_agent = NULL WHERE created_at < NOW() - INTERVAL '90 days' AND ip_address IS NOT NULL", &[])
|
.execute("UPDATE comments SET ip_address = NULL, user_agent = NULL WHERE created_at < NOW() - INTERVAL '90 days' AND ip_address IS NOT NULL", &[])
|
||||||
.await
|
.await
|
||||||
|
|||||||
@ -1,4 +1,10 @@
|
|||||||
|
//! 后台任务调度入口。
|
||||||
|
//!
|
||||||
|
//! 所有任务仅在 `server` feature 启用时编译,运行在服务端独立的 tokio 任务中。
|
||||||
|
|
||||||
|
/// 定时清理评论过期的 IP 与用户代理信息,满足隐私保护要求。
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
pub mod ip_purge;
|
pub mod ip_purge;
|
||||||
|
/// 定时删除已过期会话,避免 `sessions` 表无限增长。
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
pub mod session_cleanup;
|
pub mod session_cleanup;
|
||||||
|
|||||||
@ -1,15 +1,22 @@
|
|||||||
|
//! 会话过期清理后台任务。
|
||||||
|
//!
|
||||||
|
//! 仅在 `server` feature 启用时编译,每小时运行一次。
|
||||||
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use tokio::time::interval;
|
use tokio::time::interval;
|
||||||
|
|
||||||
use crate::db::pool::get_conn;
|
use crate::db::pool::get_conn;
|
||||||
|
|
||||||
|
/// 启动会话清理循环,每小时删除 `expires_at < NOW()` 的过期会话。
|
||||||
pub async fn run_cleanup() {
|
pub async fn run_cleanup() {
|
||||||
|
// 每小时触发一次
|
||||||
let mut ticker = interval(Duration::from_secs(3600));
|
let mut ticker = interval(Duration::from_secs(3600));
|
||||||
loop {
|
loop {
|
||||||
ticker.tick().await;
|
ticker.tick().await;
|
||||||
match get_conn().await {
|
match get_conn().await {
|
||||||
Ok(client) => {
|
Ok(client) => {
|
||||||
|
// 删除已过期会话
|
||||||
if let Err(e) = client
|
if let Err(e) = client
|
||||||
.execute("DELETE FROM sessions WHERE expires_at < NOW()", &[])
|
.execute("DELETE FROM sessions WHERE expires_at < NOW()", &[])
|
||||||
.await
|
.await
|
||||||
|
|||||||
@ -1,3 +1,9 @@
|
|||||||
|
//! 通用工具函数子模块。
|
||||||
|
//!
|
||||||
|
//! `text` 模块仅在 `server` feature 启用时编译;`time` 模块同时提供 WASM 与原生异步版本。
|
||||||
|
|
||||||
|
/// Markdown / 纯文本处理工具。
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
pub mod text;
|
pub mod text;
|
||||||
|
/// 跨平台的异步睡眠等时间工具。
|
||||||
pub mod time;
|
pub mod time;
|
||||||
|
|||||||
@ -1,26 +1,39 @@
|
|||||||
|
//! Markdown 与文本处理工具。
|
||||||
|
//!
|
||||||
|
//! 提供移除 Markdown 标记、字数统计、自动生成摘要等功能。
|
||||||
|
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
|
/// 匹配 fenced code block(```...```)的正则。
|
||||||
static CODE_BLOCK_RE: LazyLock<regex::Regex> =
|
static CODE_BLOCK_RE: LazyLock<regex::Regex> =
|
||||||
LazyLock::new(|| regex::Regex::new(r"```[\s\S]*?```").unwrap());
|
LazyLock::new(|| regex::Regex::new(r"```[\s\S]*?```").unwrap());
|
||||||
|
|
||||||
|
/// 匹配行内代码(`...`)的正则。
|
||||||
static INLINE_CODE_RE: LazyLock<regex::Regex> =
|
static INLINE_CODE_RE: LazyLock<regex::Regex> =
|
||||||
LazyLock::new(|| regex::Regex::new(r"`[^`]*`").unwrap());
|
LazyLock::new(|| regex::Regex::new(r"`[^`]*`").unwrap());
|
||||||
|
|
||||||
|
/// 匹配 Markdown 链接 `[text](url)` 的正则。
|
||||||
static LINK_RE: LazyLock<regex::Regex> =
|
static LINK_RE: LazyLock<regex::Regex> =
|
||||||
LazyLock::new(|| regex::Regex::new(r"\[([^\]]*)\]\([^)]*\)").unwrap());
|
LazyLock::new(|| regex::Regex::new(r"\[([^\]]*)\]\([^)]*\)").unwrap());
|
||||||
|
|
||||||
|
/// 匹配 Markdown 标题(# 到 ######)的正则。
|
||||||
static HEADING_RE: LazyLock<regex::Regex> =
|
static HEADING_RE: LazyLock<regex::Regex> =
|
||||||
LazyLock::new(|| regex::Regex::new(r"^#{1,6}\s*").unwrap());
|
LazyLock::new(|| regex::Regex::new(r"^#{1,6}\s*").unwrap());
|
||||||
|
|
||||||
|
/// 匹配 Markdown 图片 `` 的正则。
|
||||||
static IMAGE_RE: LazyLock<regex::Regex> =
|
static IMAGE_RE: LazyLock<regex::Regex> =
|
||||||
LazyLock::new(|| regex::Regex::new(r"!\[([^\]]*)\]\([^)]*\)").unwrap());
|
LazyLock::new(|| regex::Regex::new(r"!\[([^\]]*)\]\([^)]*\)").unwrap());
|
||||||
|
|
||||||
|
/// 匹配任意空白字符的正则,用于把多个空白合并为单个空格。
|
||||||
static WHITESPACE_RE: LazyLock<regex::Regex> = LazyLock::new(|| regex::Regex::new(r"\s+").unwrap());
|
static WHITESPACE_RE: LazyLock<regex::Regex> = LazyLock::new(|| regex::Regex::new(r"\s+").unwrap());
|
||||||
|
|
||||||
|
/// 去除 Markdown 标记,返回近似纯文本。
|
||||||
|
///
|
||||||
|
/// 处理顺序:代码块 → 行内代码 → 图片 → 链接(保留文字)→ 标题 → 加粗/斜体 → 合并空白。
|
||||||
pub fn strip_markdown(md: &str) -> String {
|
pub fn strip_markdown(md: &str) -> String {
|
||||||
let mut plain = CODE_BLOCK_RE.replace_all(md, "").to_string();
|
let mut plain = CODE_BLOCK_RE.replace_all(md, "").to_string();
|
||||||
plain = INLINE_CODE_RE.replace_all(&plain, "").to_string();
|
plain = INLINE_CODE_RE.replace_all(&plain, "").to_string();
|
||||||
// Must strip images BEFORE links, otherwise `` becomes `!`
|
// 必须先移除图片再处理链接,否则 `` 会残留 `!`
|
||||||
plain = IMAGE_RE.replace_all(&plain, "").to_string();
|
plain = IMAGE_RE.replace_all(&plain, "").to_string();
|
||||||
plain = LINK_RE.replace_all(&plain, "$1").to_string();
|
plain = LINK_RE.replace_all(&plain, "$1").to_string();
|
||||||
plain = HEADING_RE.replace_all(&plain, "").to_string();
|
plain = HEADING_RE.replace_all(&plain, "").to_string();
|
||||||
@ -33,12 +46,17 @@ pub fn strip_markdown(md: &str) -> String {
|
|||||||
plain.trim().to_string()
|
plain.trim().to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 统计 Markdown 文本的有效字数。
|
||||||
|
///
|
||||||
|
/// 中文字符每个计 1;英文字母按连续字母串计 1 个词。
|
||||||
|
/// 空文本返回 1,避免摘要或列表中出现 0 字的显示问题。
|
||||||
pub fn count_words(md: &str) -> u32 {
|
pub fn count_words(md: &str) -> u32 {
|
||||||
let plain = strip_markdown(md);
|
let plain = strip_markdown(md);
|
||||||
let mut count = 0u32;
|
let mut count = 0u32;
|
||||||
let mut in_word = false;
|
let mut in_word = false;
|
||||||
|
|
||||||
for c in plain.chars() {
|
for c in plain.chars() {
|
||||||
|
// CJK 统一表意文字范围(基本区)
|
||||||
if c as u32 >= 0x4E00 && c as u32 <= 0x9FFF {
|
if c as u32 >= 0x4E00 && c as u32 <= 0x9FFF {
|
||||||
count += 1;
|
count += 1;
|
||||||
in_word = false;
|
in_word = false;
|
||||||
@ -54,6 +72,7 @@ pub fn count_words(md: &str) -> u32 {
|
|||||||
count.max(1)
|
count.max(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 自动生成文本摘要,取去除 Markdown 后的前 200 个字符。
|
||||||
pub fn auto_summary(md: &str) -> String {
|
pub fn auto_summary(md: &str) -> String {
|
||||||
let plain = strip_markdown(md);
|
let plain = strip_markdown(md);
|
||||||
plain.chars().take(200).collect()
|
plain.chars().take(200).collect()
|
||||||
|
|||||||
@ -1,3 +1,10 @@
|
|||||||
|
//! 跨平台时间/睡眠工具。
|
||||||
|
//!
|
||||||
|
//! 根据目标架构分别实现:
|
||||||
|
//! - `wasm32`:通过 `js_sys` 调用 JavaScript 的 `setTimeout`。
|
||||||
|
//! - 其他平台:使用 `tokio::time::sleep`。
|
||||||
|
|
||||||
|
/// 异步睡眠指定毫秒数。
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
pub async fn sleep_ms(ms: u32) {
|
pub async fn sleep_ms(ms: u32) {
|
||||||
use wasm_bindgen::JsCast;
|
use wasm_bindgen::JsCast;
|
||||||
@ -9,6 +16,7 @@ pub async fn sleep_ms(ms: u32) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 异步睡眠指定毫秒数(原生 tokio 版本)。
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub async fn sleep_ms(ms: u32) {
|
pub async fn sleep_ms(ms: u32) {
|
||||||
tokio::time::sleep(std::time::Duration::from_millis(ms as u64)).await;
|
tokio::time::sleep(std::time::Duration::from_millis(ms as u64)).await;
|
||||||
|
|||||||
36
src/webp.rs
36
src/webp.rs
@ -1,10 +1,27 @@
|
|||||||
|
//! WebP 编解码模块。
|
||||||
|
//!
|
||||||
|
//! 本模块仅在 `server` feature 启用时编译。
|
||||||
|
//!
|
||||||
|
//! ## `zenwebp` 与 `image` crate 的分工
|
||||||
|
//!
|
||||||
|
//! - `image` crate:负责通用图像格式(JPEG、PNG、GIF 等)的解码、缩放、旋转以及
|
||||||
|
//! 像素格式转换(`DynamicImage`、`RgbaImage`、`RgbImage`)。本项目特意禁用了 `image`
|
||||||
|
//! 的 `webp` feature,因为它不支持 WebP 编码,且解码能力有限。
|
||||||
|
//! - `zenwebp`:专门负责 WebP 格式的有损编码与解码。所有需要输出 WebP 或读取 WebP
|
||||||
|
//! 字节流的场景都通过 `zenwebp` 完成。
|
||||||
|
//!
|
||||||
|
//! 简言之:`image` 处理“除 WebP 外的图像操作”,`zenwebp` 处理“WebP 专有编解码”。
|
||||||
|
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
|
/// WebP 编解码过程中可能产生的错误。
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum WebpError {
|
pub enum WebpError {
|
||||||
|
/// 编码失败。
|
||||||
Encode(String),
|
Encode(String),
|
||||||
|
/// 解码失败。
|
||||||
Decode(String),
|
Decode(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -21,13 +38,20 @@ impl std::fmt::Display for WebpError {
|
|||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
impl std::error::Error for WebpError {}
|
impl std::error::Error for WebpError {}
|
||||||
|
|
||||||
|
/// WebP 有损编码配置。
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct WebpConfig {
|
pub struct WebpConfig {
|
||||||
|
/// 质量系数,范围 0.0–100.0。
|
||||||
pub quality: f32,
|
pub quality: f32,
|
||||||
|
/// 编码方法,范围 0–6,数值越大压缩率越高但越慢。
|
||||||
pub method: u8,
|
pub method: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 从环境变量读取的 WebP 全局配置,未设置时使用默认值。
|
||||||
|
///
|
||||||
|
/// - `WEBP_QUALITY`:默认 85.0,越界时 clamp 到 0.0–100.0。
|
||||||
|
/// - `WEBP_METHOD`:默认 2,越界时 clamp 到 0–6。
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
pub static WEBP_CONFIG: LazyLock<WebpConfig> = LazyLock::new(|| {
|
pub static WEBP_CONFIG: LazyLock<WebpConfig> = LazyLock::new(|| {
|
||||||
let (quality, quality_clamped) = std::env::var("WEBP_QUALITY")
|
let (quality, quality_clamped) = std::env::var("WEBP_QUALITY")
|
||||||
@ -68,6 +92,9 @@ pub static WEBP_CONFIG: LazyLock<WebpConfig> = LazyLock::new(|| {
|
|||||||
WebpConfig { quality, method }
|
WebpConfig { quality, method }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// 将 `image::DynamicImage` 编码为 WebP 字节流。
|
||||||
|
///
|
||||||
|
/// 直接处理 `Rgba8` 与 `Rgb8` 两种像素布局,其他格式先转换为 `Rgba8` 再编码。
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
pub fn encode(img: &image::DynamicImage, quality: f32, method: u8) -> Result<Vec<u8>, WebpError> {
|
pub fn encode(img: &image::DynamicImage, quality: f32, method: u8) -> Result<Vec<u8>, WebpError> {
|
||||||
use zenwebp::{EncodeRequest, LossyConfig, PixelLayout};
|
use zenwebp::{EncodeRequest, LossyConfig, PixelLayout};
|
||||||
@ -95,13 +122,17 @@ pub fn encode(img: &image::DynamicImage, quality: f32, method: u8) -> Result<Vec
|
|||||||
do_encode(&config, rgb.as_raw(), PixelLayout::Rgb8, width, height)
|
do_encode(&config, rgb.as_raw(), PixelLayout::Rgb8, width, height)
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
// Convert other formats to RGBA8
|
// 其他像素格式统一转换为 RGBA8 后再交给 zenwebp 编码
|
||||||
let rgba = img.to_rgba8();
|
let rgba = img.to_rgba8();
|
||||||
do_encode(&config, rgba.as_raw(), PixelLayout::Rgba8, width, height)
|
do_encode(&config, rgba.as_raw(), PixelLayout::Rgba8, width, height)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 将 WebP 字节流解码为 `image::DynamicImage`。
|
||||||
|
///
|
||||||
|
/// 根据 alpha 通道信息决定返回 `ImageRgba8` 还是 `ImageRgb8`。
|
||||||
|
/// 解码前会校验像素总数,防止超大图片导致内存问题。
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
pub fn decode(data: &[u8]) -> Result<image::DynamicImage, WebpError> {
|
pub fn decode(data: &[u8]) -> Result<image::DynamicImage, WebpError> {
|
||||||
use zenwebp::WebPDecoder;
|
use zenwebp::WebPDecoder;
|
||||||
@ -116,6 +147,7 @@ pub fn decode(data: &[u8]) -> Result<image::DynamicImage, WebpError> {
|
|||||||
|
|
||||||
let pixel_count = (width as u64) * (height as u64);
|
let pixel_count = (width as u64) * (height as u64);
|
||||||
|
|
||||||
|
// 超过最大允许像素数时提前拒绝
|
||||||
if pixel_count > crate::api::image::MAX_IMAGE_PIXELS as u64 {
|
if pixel_count > crate::api::image::MAX_IMAGE_PIXELS as u64 {
|
||||||
return Err(WebpError::Decode(format!(
|
return Err(WebpError::Decode(format!(
|
||||||
"Image dimensions {}x{} exceed maximum allowed pixels",
|
"Image dimensions {}x{} exceed maximum allowed pixels",
|
||||||
@ -137,7 +169,7 @@ pub fn decode(data: &[u8]) -> Result<image::DynamicImage, WebpError> {
|
|||||||
.map(image::DynamicImage::ImageRgba8)
|
.map(image::DynamicImage::ImageRgba8)
|
||||||
.ok_or_else(|| WebpError::Decode("Invalid RGBA dimensions".to_string()))
|
.ok_or_else(|| WebpError::Decode("Invalid RGBA dimensions".to_string()))
|
||||||
} else {
|
} else {
|
||||||
// For RGB output, the buffer is width * height * 3
|
// 无 alpha 时,zenwebp 输出的是 width * height * 3 的 RGB 数据
|
||||||
image::RgbImage::from_raw(width, height, output)
|
image::RgbImage::from_raw(width, height, output)
|
||||||
.map(image::DynamicImage::ImageRgb8)
|
.map(image::DynamicImage::ImageRgb8)
|
||||||
.ok_or_else(|| WebpError::Decode("Invalid RGB dimensions".to_string()))
|
.ok_or_else(|| WebpError::Decode("Invalid RGB dimensions".to_string()))
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user