Files
endorphin-tessera-gui/src/render/image.rs
T

267 lines
14 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.
//! 有界不可变 RGBA 图片资源,由原生 Arc 共享,准备阶段解码/排字,逐帧软件渲染只采样像素。
//! 调用路径:graphics 的图像加载入口调用 png,text_flow 调用 text 缓存设备分辨率字词。
//! SoftwareRenderer.draw_image 逆映射目标像素后调用 sample;graphics 使用 outline 取得排版避让轮廓。
//! id 只用于原生缓存身份比较,不是 VM 句柄,也不表示两个相同像素资源必有相同 ID。
//!
//! Immutable, bounded RGBA images shared by display lists. Decoding and text
//! preparation happen outside the frame loop; the CPU backend samples them.
use crate::{
display_list::Color,
geometry::Rect,
text::{self, TextStyle},
};
use std::{io::Cursor, sync::Arc};
#[derive(Debug, serde::Serialize)]
// 像素、尺寸和轮廓在构造完成后由 Arc 只读共享;序列化省略原始像素与轮廓。
pub struct Image {
// 资源实际像素宽,png/text 构造保证至少为一。
pub width: u32,
// 资源实际像素高,与绘制局部逻辑 rect 可按 scale 不同。
pub height: u32,
// 每次准备新资源取得的进程内数值身份,PartialEq 只比较它以便快速判断缓存复用。
pub id: u64,
#[serde(skip)]
// 行优先 RGBA 字节数组,保存 straight alpha;采样时临时转预乘权重避免透明边黑晕。
pixels: Vec<u8>,
#[serde(skip)]
// PNG 的 alpha 保守凸包,用于图形排版避让;预渲染文字目前返回空轮廓。
pub outline: Vec<crate::geometry::Point>,
}
impl Image {
// 将受预算约束的 PNG 解码为 RGBA,并一次性生成透明轮廓;格式/解码错误直接交给加载调用方。
pub fn png(bytes: &[u8]) -> anyhow::Result<Arc<Self>> {
// 借用输入字节建立内存解码器,不在帧循环执行文件 I/O。
let mut decoder = png::Decoder::new(Cursor::new(bytes));
// 展开调色板/低位深并去掉 16 位低字节,使后面每个颜色样本可按 8 位通道读取。
decoder.set_transformations(png::Transformations::EXPAND | png::Transformations::STRIP_16);
// 先读取 PNG 头,像素预算在分配输出缓冲前检查。
let mut reader = decoder.read_info()?;
// 头部尺寸非零且总像素不超过 4194304,使用 u64 乘法避免 u32 乘积溢出。
let info = reader.info();
anyhow::ensure!(
info.width > 0
&& info.height > 0
&& u64::from(info.width) * u64::from(info.height) <= 4_194_304,
"image exceeds pixel budget"
);
// 按解码器计算的输出字节数分配,计算溢出时拒绝而非猜测缓冲长度。
let mut buffer = vec![
0;
reader
.output_buffer_size()
.ok_or_else(|| anyhow::anyhow!("image size overflow"))?
];
// 解码一帧并取得实际输出布局,只消费 buffer_size 范围,不读取未写尾部。
let info = reader.next_frame(&mut buffer)?;
// 预留统一四通道容量,原 PNG 可以是灰度、RGB 或 RGBA。
let mut pixels = Vec::with_capacity(info.width as usize * info.height as usize * 4);
// 按解码后的每像素样本数遍历,所有格式归并成同一 RGBA 存储。
for chunk in buffer[..info.buffer_size()].chunks_exact(info.color_type.samples()) {
// 灰度复制到 RGB,缺 alpha 填 255;若 Indexed 未展开则报错,不能把索引当颜色通道。
let rgba = match info.color_type {
// 原生四通道直接复制,保留源 alpha。
// 三通道图片默认全不透明,不从相邻像素误读 alpha。
png::ColorType::Rgba => [chunk[0], chunk[1], chunk[2], chunk[3]],
png::ColorType::Rgb => [chunk[0], chunk[1], chunk[2], 255],
// 单灰度通道复制到三色,完全不透明。
png::ColorType::Grayscale => [chunk[0], chunk[0], chunk[0], 255],
// 灰度/alpha 双通道保留透明度,前三色仍同值。
png::ColorType::GrayscaleAlpha => [chunk[0], chunk[0], chunk[0], chunk[1]],
// EXPAND 后正常不应残留索引图,若仍存在就拒绝不支持的输出布局。
png::ColorType::Indexed => anyhow::bail!("unexpanded indexed PNG"),
};
pixels.extend_from_slice(&rgba);
}
// 资源准备阶段扫描 alpha 生成保守轮廓,后续排版不必每帧重新遍历图片像素。
let outline = alpha_hull(&pixels, info.width, info.height);
Ok(Arc::new(Self {
width: info.width,
height: info.height,
pixels,
id: next_id(),
outline,
}))
}
/// Cache a shaped word at the actual device resolution. Padding preserves
/// italic/serif overhangs. Returns its logical image rectangle and advance.
// 按实际设备 scale 预渲染一段已排字文本,返回共享资源、逻辑图片框和不含 padding 的文字 advance。
pub fn text(
value: &str,
style: TextStyle,
scale: f32,
color: Color,
) -> Result<(Arc<Self>, Rect, f32), &'static str> {
// 用逻辑 TextStyle 测量自然字形 advance/行高;返回 advance 不随设备 DPI 改变。
let size = text::measure(value, style);
// 按字号预留约 30% 整数逻辑边距,容纳斜体/衬线伸出而不裁掉字形。
let pad = (style.font_size * 0.3).ceil();
// 两侧 padding 后乘设备比例再向上取整,实际资源至少一像素宽。
let width = ((size.width + pad * 2.) * scale).ceil().max(1.) as u32;
// 高度也含两侧留白并按设备比例取整,与 width 分别计算。
let height = ((size.height + pad * 2.) * scale).ceil().max(1.) as u32;
// 准备文字资源另限 1048576 像素,避免单段文本缓冲无限增长。
if u64::from(width) * u64::from(height) > 1_048_576 {
return Err("prepared text image exceeds pixel budget");
}
// 零初始化 RGBA 即透明背景,字形回调只修改实际覆盖的像素。
let mut pixels = vec![0; width as usize * height as usize * 4];
// 以设备字号绘制,负 padding 框与下面回调偏移共同把逻辑文字原点放在资源留白之后。
text::draw_clipped(
value,
style.scaled(scale),
color,
None,
Rect::new(-pad * scale, -pad * scale, width as f32, height as f32),
|x, y, c| {
// 把绘制回调坐标移到含左 padding 的缓冲位置,按设备像素舍入。
let x = x + (pad * scale).round() as i32;
// 纵向也加 top padding,保持字形基线与返回的逻辑图片框一致。
let y = y + (pad * scale).round() as i32;
// 排除所有缓冲外像素,防止字形 overhang 或舍入造成越界写入。
if x < 0 || y < 0 || x >= width as i32 || y >= height as i32 {
return;
}
// 行优先像素索引乘四得到 RGBA 字节起点,先通过上面坐标范围检查。
let i = (y as usize * width as usize + x as usize) * 4;
// All glyphs have the same ink color; alpha-over preserves overlaps.
pixels[i..i + 3].copy_from_slice(&[c.red, c.green, c.blue]);
pixels[i + 3] = (u16::from(c.alpha)
+ u16::from(pixels[i + 3]) * (255 - u16::from(c.alpha)) / 255)
as u8;
},
);
// 返回的新图有独立 id;逻辑 rect 左上为负 padding,绘图能保持原文字基线与 advance 对齐。
Ok((
Arc::new(Self {
width,
height,
pixels,
id: next_id(),
outline: vec![],
}),
Rect::new(-pad, -pad, width as f32 / scale, height as f32 / scale),
size.width,
))
}
// 软件渲染器传入逆映射的归一化纹理点和已夹取 opacity;预乘 alpha 双线性插值后返回 straight RGBA。
pub fn sample(&self, u: f32, v: f32, opacity: f32) -> Color {
// 归一化 u 映射到像素中心索引,减 0.5 使 texel 中心落在整数坐标。
let x = u * self.width as f32 - 0.5;
// v 使用同样中心约定,不把资源边缘当作像素中心。
let y = v * self.height as f32 - 0.5;
// 取左上采样格的整数基点,四个邻居随后夹在资源边界。
let (ix, iy) = (x.floor() as i32, y.floor() as i32);
// 小数部分作为双线性权重,四项权重之和为一。
let (fx, fy) = (x - x.floor(), y - y.floor());
// 前三项累计 RGB×alpha×权重,第四项累计 alpha×权重,避免透明像素 RGB 污染边缘。
let mut sum = [0.; 4];
// 四邻像素按二维线性乘积权重采样,边缘通过 clamping 复制最近 texel。
for (dx, dy, w) in [
(0, 0, (1. - fx) * (1. - fy)),
(1, 0, fx * (1. - fy)),
(0, 1, (1. - fx) * fy),
(1, 1, fx * fy),
] {
// 横轴邻居夹到有效像素列,不在透明边缘之外读越界数据。
let px = (ix + dx).clamp(0, self.width as i32 - 1) as usize;
// 纵轴独立夹取,图片宽高不同也不能复用同一边界。
let py = (iy + dy).clamp(0, self.height as i32 - 1) as usize;
// 转换资源像素位置为四通道字节偏移。
let i = (py * self.width as usize + px) * 4;
// alpha 保留 0..255 数值再乘权重,前三色同用该权重以实现预乘插值。
let a = f32::from(self.pixels[i + 3]) * w;
// 只累计 RGB 三通道,最终 alpha 单独累加,避免重复乘整体 opacity。
for (j, s) in sum.iter_mut().enumerate().take(3) {
*s += f32::from(self.pixels[i + j]) * a;
}
sum[3] += a;
}
// 完全透明时直接返回透明黑,避免除以零且不泄露透明像素的无意义 RGB。
if sum[3] <= 0. {
return Color::rgba(0, 0, 0, 0);
}
// RGB 除累计 alpha 恢复 straight 通道,整体 opacity 只作用最终 alpha。
Color::rgba(
(sum[0] / sum[3]).round() as u8,
(sum[1] / sum[3]).round() as u8,
(sum[2] / sum[3]).round() as u8,
(sum[3] * opacity).round() as u8,
)
}
}
/// Conservative convex silhouette from opaque row endpoints. Unlike an image's
/// rectangle, this excludes empty transparent margins. Generated once per PNG.
fn alpha_hull(pixels: &[u8], width: u32, height: u32) -> Vec<crate::geometry::Point> {
use crate::geometry::Point;
// 先收集各有效行左右端像素的四角点,后面求整体凸包,而非逐像素精确边界。
let mut points = Vec::new();
// 按行扫描 alpha,阈值 16 去掉几乎透明的外围像素。
for y in 0..height {
// 只保留 alpha>=16 的列号,颜色通道不影响避让轮廓。
let xs: Vec<_> = (0..width)
.filter(|&x| pixels[((y * width + x) * 4 + 3) as usize] >= 16)
.collect();
// 该行有有效像素才生成候选角点,空透明行不参与外形。
if let (Some(&left), Some(&right)) = (xs.first(), xs.last()) {
// 右端取 right+1 表达像素外边界,不把末列中心当作轮廓右缘。
for x in [left as f32, right as f32 + 1.] {
// 上下各取像素行边界,保守覆盖整个端点像素单元。
for yy in [y as f32, y as f32 + 1.] {
points.push(Point::new(x, yy));
}
}
}
}
// 按 x/y 全序排序,后面单调链要求稳定几何次序;随后去掉重复角点。
points.sort_by(|a, b| a.x.total_cmp(&b.x).then(a.y.total_cmp(&b.y)));
points.dedup();
// 不足三点直接返回,无法构成面积凸多边形,不补造图片矩形。
if points.len() < 3 {
return points;
}
// 二维叉积符号判断转向,单调链移除非凸与共线中间点。
let cross =
|a: Point, b: Point, c: Point| (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
// 结果栈先构造下链,再逆序追加上链。
let mut hull: Vec<Point> = Vec::new();
// 按升序构造第一条凸包链,每个候选最多入栈/出栈一次。
for &p in &points {
// 新点使末两点不构成严格左转时弹出中间点,保留外侧端点。
while hull.len() >= 2 && cross(hull[hull.len() - 2], hull[hull.len() - 1], p) <= 0. {
hull.pop();
}
hull.push(p);
}
// 记录下链末端位置,构造上链时不能越过这条边界。
let lower = hull.len();
// 反向跳过已在下链末端的最右点,继续构造上链并闭合。
for &p in points.iter().rev().skip(1) {
// 与下链相同转向规则,但保留已完成下链;最后会去掉重复闭合首点。
while hull.len() > lower && cross(hull[hull.len() - 2], hull[hull.len() - 1], p) <= 0. {
hull.pop();
}
hull.push(p);
}
// 首点在上下链闭合时出现两次,移除末尾重复点,返回不含闭合副本的凸包。
hull.pop();
hull
}
// 为每个准备完成的原生资源分配进程内身份;只需要原子递增取号,不承担读写像素的跨线程发布屏障。
fn next_id() -> u64 {
// 计数器从一开始,Relaxed 只用于取号;资源的安全共享由不可变 Arc 实现。
static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
}
impl PartialEq for Image {
// 原生缓存比较资源身份而非扫描像素;两次解码相同文件也会是不同资源 ID。
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}