//! 有界不可变 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, #[serde(skip)] // PNG 的 alpha 保守凸包,用于图形排版避让;预渲染文字目前返回空轮廓。 pub outline: Vec, } impl Image { // 将受预算约束的 PNG 解码为 RGBA,并一次性生成透明轮廓;格式/解码错误直接交给加载调用方。 pub fn png(bytes: &[u8]) -> anyhow::Result> { // 借用输入字节建立内存解码器,不在帧循环执行文件 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, 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 { 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 = 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 } }