release: v0.2.5
CI / fmt (rustfmt --check) (push) Waiting to run
CI / clippy (ubuntu-latest) (push) Waiting to run
CI / clippy (windows-latest) (push) Waiting to run
CI / test (ubuntu-latest) (push) Waiting to run
CI / test (windows-latest) (push) Waiting to run
CI / build release (ubuntu-latest) (push) Waiting to run
CI / build release (windows-latest) (push) Waiting to run
CI / runtime smoke (ubuntu x64, xvfb) (push) Waiting to run
CI / runtime smoke (macos arm64, probe) (push) Skipped
CI / update-e2e (windows x64) (push) Waiting to run
GPU evidence / GPU 判据自测(不需 GPU;跳过 ≠ 已验证) (push) Waiting to run
GPU evidence / GPU 证据 — 探测可用产物与硬件适配器 (push) Waiting to run
Guards / core 零 UI 守门 (push) Waiting to run
Guards / presenter 零 UI 守门 (push) Waiting to run
Guards / design 零第三方依赖守门 (push) Waiting to run
Guards / unsafe 落点守门(集合必须恰好等于期望) (push) Waiting to run
Guards / 文件行数 ≤400 守门 (push) Waiting to run
Guards / 依赖许可与来源(cargo-deny) (push) Waiting to run
Release guard / 发布标记语义守门(push tag → 稳定版) (push) Waiting to run
Signing status(播报,不是门禁) / 判据自测(三行/退出码/变异重放/接线) (push) Waiting to run
Signing status(播报,不是门禁) / 签名就绪度(播报) (push) Waiting to run
Release / 选槽位(默认四个:Windows 两档 + Linux 两档) (push) Waiting to run

This commit is contained in:
AskaEth committed 2026-11-12 12:06:59 +08:00
commit 0522a41a52
1587 files changed
+323674

No files matched your search

+57
View File
@@ -0,0 +1,57 @@
//! 缩略图:路径 → 限尺寸的 [`Rgba8`](D54 第 1 条,M251 · SAVESHOTS)。
//!
//! ## 为什么落在本 crate
//!
//! D54 定的分层没有变:图片解码**集中在 `palette-extract`**(`crates/design` 零第三方依赖的
//! 代价就是把解码摊到这里;`app-iced` 另有一份 `image` 只用于截图编码与背景图,见它自己的
//! `Cargo.toml` 注释)。截图是 PNG,本 crate 的 `image` 已经开了 `png`/`jpeg`/`bmp` 解码 feature,
//! 所以加这条路**零新依赖、零新 crate、零新 feature**——只是把「解码 + 限尺寸」这段已有的能力
//! 换个目标边长([`THUMBNAIL_MAX_EDGE`])暴露一次。
//!
//! ## 不落盘
//!
//! 这里**只返回像素**,不写任何缓存文件(D54 第 2 条):写 PNG 需要编码器,而编码器只为一份
//! 磁盘缓存引入不划算;内存缓存归调用方(presenter 的 `host::saves::thumbs`)。
//!
//! ## 失败不 panic
//!
//! 读不到 / 不是图片 / 头里写着 40000×40000,全都变成 [`ExtractError`](与取色那条路同一族
//! 错误)。调用方据此给「逐条可见」的失败原因——**不许**在这里「读不到就画个灰块」。
//!
//! 与 [`crate::load`] 的区别只有一条:目标边长。`load` 钉 `MAX_EDGE = 256`(取色只需要色彩分布),
//! 这里由调用方给(缩略图墙用 160)。
//!
//! ```no_run
//! use unlucky_palette_extract::{thumbnail, THUMBNAIL_MAX_EDGE};
//!
//! match thumbnail("screenshots/2024-05-01_10.00.00.png", THUMBNAIL_MAX_EDGE) {
//! Ok(thumb) => println!("{}×{}", thumb.width(), thumb.height()),
//! Err(error) => eprintln!("这张图没有缩略图:{error}"),
//! }
//! ```
use std::path::Path;
use crate::{decode_with_limits, fit, ExtractError, Rgba8};
/// 缩略图的最大边(像素)。D54 定 160。
///
/// 依据:一屏缩略图墙按 4 列排、详情列宽约 700px ⇒ 每格 ≈ 160px。再大只是多占内存
/// (160² × 4B ≈ 100KB/张,60 张 ≈ 6MB),肉眼在墙上看不出差别。
pub const THUMBNAIL_MAX_EDGE: u32 = 160;
/// 从文件路径解码并缩到**长边 ≤ `max_edge`**(等比、不放大),返回 RGBA8 像素。
///
/// 格式按**魔数**嗅探,不看扩展名(与 [`crate::load`] 同一条路:用户把 `.jpg` 改名成 `.png`
/// 也照样读得出来)。`max_edge = 0` 按 1 处理(防调用方传 0 得到 0×0 的图)。
///
/// # Errors
///
/// - [`ExtractError::Io`]:文件不存在 / 无权限 / 被占用 / 打不开;
/// - [`ExtractError::Decode`]:不是认得的图片、文件截断、或头尺寸超过 16384(解压炸弹)。
pub fn thumbnail(path: impl AsRef<Path>, max_edge: u32) -> Result<Rgba8, ExtractError> {
let max_edge = max_edge.max(1);
let reader = image::ImageReader::open(path).map_err(ExtractError::Io)?;
let reader = reader.with_guessed_format().map_err(ExtractError::Io)?;
Ok(fit(decode_with_limits(reader)?, max_edge))
}