feat(palette-extract): 加 thumbnail(path, max_edge) 出限尺寸缩略图(M251 · D54)

D54 第 1 条:解码与缩放留在 palette-extract(截图是 PNG,现有 png/jpeg/bmp
解码 feature 已覆盖,零新依赖、零新 crate、零新 feature)。

- 把 finish() 拆成 decode_with_limits()(读 + 嗅探 + 卡解压炸弹)与 fit()(等比缩、
  不放大),取色与缩略图共用同一段解码,只是目标边长不同;
- 新增 thumbnail(path, max_edge) -> Result<Rgba8, ExtractError>,失败给结构化错误
  (Io / Decode),不 panic、不返回灰图;max_edge=0 按 1 处理;
- THUMBNAIL_MAX_EDGE = 160(D54 第 5 条)。

用例 5 条:真 PNG 等比缩到给定长边、小图不放大、坏 PNG 给 Decode、缺文件给 Io、
0 边按 1 处理。
This commit is contained in:
AskaEth committed 2026-11-11 16:15:37 +08:00
1 parent 5487f661d9
commit 798bfb279b
3 files changed
+174 -8

No files matched your search

+27 -8
View File
@@ -64,6 +64,10 @@ use std::path::Path;
use unlucky_design::palette::{self, ImageInput, ImageSource, Seed};
mod thumbnail;
pub use thumbnail::{thumbnail, THUMBNAIL_MAX_EDGE};
/// 交给 `unlucky_design` 前允许的最大边长(像素)。
///
/// 256 的依据:量化器自己只采 65536 个点(≈256²),再大的图在 `crates/design` 里也会被
@@ -186,31 +190,46 @@ pub fn input<'a>(loaded: &'a Result<Rgba8, ExtractError>) -> ImageInput<'a> {
}
fn finish<R: std::io::BufRead + std::io::Seek>(
mut reader: image::ImageReader<R>,
reader: image::ImageReader<R>,
) -> Result<Rgba8, ExtractError> {
Ok(fit(decode_with_limits(reader)?, MAX_EDGE))
}
/// 解码一个 reader(带 [`MAX_SOURCE_EDGE`] 头尺寸上限),**不缩放**。
///
/// 从 [`finish`] 里拆出来只有一个理由:取色([`load`])与缩略图([`thumbnail`])要**不同的
/// 目标边长**,而「读文件 + 嗅探格式 + 卡解压炸弹」这段必须只有一份(两份就会有一份忘了卡上限)。
pub(crate) fn decode_with_limits<R: std::io::BufRead + std::io::Seek>(
mut reader: image::ImageReader<R>,
) -> Result<image::RgbaImage, ExtractError> {
let mut limits = image::Limits::default();
limits.max_image_width = Some(MAX_SOURCE_EDGE);
limits.max_image_height = Some(MAX_SOURCE_EDGE);
reader.limits(limits);
let decoded = reader.decode().map_err(ExtractError::Decode)?;
let rgba = decoded.to_rgba8();
Ok(decoded.to_rgba8())
}
/// 把解码结果等比缩到**长边 ≤ `max_edge`**(不放大:本来就小的图原样交付)。
///
/// `thumbnail` 是整数盒式采样:对取色(只关心色彩分布)与缩略图(160px 的墙)都够,
/// 且比 Lanczos 快一个量级。
pub(crate) fn fit(rgba: image::RgbaImage, max_edge: u32) -> Rgba8 {
let (width, height) = (rgba.width(), rgba.height());
let scaled = if width.max(height) > MAX_EDGE {
// 等比缩到长边 = MAX_EDGE。`thumbnail` 是整数快速采样,对取色(只关心色彩分布)
// 足够,且比 Lanczos 快一个量级。
let factor = f64::from(MAX_EDGE) / f64::from(width.max(height));
let scaled = if width.max(height) > max_edge {
let factor = f64::from(max_edge) / f64::from(width.max(height));
let target_width = ((f64::from(width) * factor).round() as u32).max(1);
let target_height = ((f64::from(height) * factor).round() as u32).max(1);
image::imageops::thumbnail(&rgba, target_width, target_height)
} else {
rgba
};
Ok(Rgba8 {
Rgba8 {
width: scaled.width(),
height: scaled.height(),
pixels: scaled.into_raw(),
})
}
}
#[cfg(test)]