267 lines
14 KiB
Rust
267 lines
14 KiB
Rust
//! 有界不可变 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
|
||
}
|
||
}
|