194 lines
7.0 KiB
Rust
194 lines
7.0 KiB
Rust
//! 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)]
|
|
pub struct Image {
|
|
pub width: u32,
|
|
pub height: u32,
|
|
pub id: u64,
|
|
#[serde(skip)]
|
|
pixels: Vec<u8>,
|
|
#[serde(skip)]
|
|
pub outline: Vec<crate::geometry::Point>,
|
|
}
|
|
|
|
impl Image {
|
|
pub fn png(bytes: &[u8]) -> anyhow::Result<Arc<Self>> {
|
|
let mut decoder = png::Decoder::new(Cursor::new(bytes));
|
|
decoder.set_transformations(png::Transformations::EXPAND | png::Transformations::STRIP_16);
|
|
let mut reader = decoder.read_info()?;
|
|
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"))?
|
|
];
|
|
let info = reader.next_frame(&mut buffer)?;
|
|
let mut pixels = Vec::with_capacity(info.width as usize * info.height as usize * 4);
|
|
for chunk in buffer[..info.buffer_size()].chunks_exact(info.color_type.samples()) {
|
|
let rgba = match info.color_type {
|
|
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],
|
|
png::ColorType::GrayscaleAlpha => [chunk[0], chunk[0], chunk[0], chunk[1]],
|
|
png::ColorType::Indexed => anyhow::bail!("unexpanded indexed PNG"),
|
|
};
|
|
pixels.extend_from_slice(&rgba);
|
|
}
|
|
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.
|
|
pub fn text(
|
|
value: &str,
|
|
style: TextStyle,
|
|
scale: f32,
|
|
color: Color,
|
|
) -> Result<(Arc<Self>, Rect, f32), &'static str> {
|
|
let size = text::measure(value, style);
|
|
let pad = (style.font_size * 0.3).ceil();
|
|
let width = ((size.width + pad * 2.) * scale).ceil().max(1.) as u32;
|
|
let height = ((size.height + pad * 2.) * scale).ceil().max(1.) as u32;
|
|
if u64::from(width) * u64::from(height) > 1_048_576 {
|
|
return Err("prepared text image exceeds pixel budget");
|
|
}
|
|
let mut pixels = vec![0; width as usize * height as usize * 4];
|
|
text::draw_clipped(
|
|
value,
|
|
style.scaled(scale),
|
|
color,
|
|
None,
|
|
Rect::new(-pad * scale, -pad * scale, width as f32, height as f32),
|
|
|x, y, c| {
|
|
let x = x + (pad * scale).round() as i32;
|
|
let y = y + (pad * scale).round() as i32;
|
|
if x < 0 || y < 0 || x >= width as i32 || y >= height as i32 {
|
|
return;
|
|
}
|
|
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;
|
|
},
|
|
);
|
|
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,
|
|
))
|
|
}
|
|
|
|
pub fn sample(&self, u: f32, v: f32, opacity: f32) -> Color {
|
|
let x = u * self.width as f32 - 0.5;
|
|
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());
|
|
let mut sum = [0.; 4];
|
|
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;
|
|
let a = f32::from(self.pixels[i + 3]) * w;
|
|
for (j, s) in sum.iter_mut().enumerate().take(3) {
|
|
*s += f32::from(self.pixels[i + j]) * a;
|
|
}
|
|
sum[3] += a;
|
|
}
|
|
if sum[3] <= 0. {
|
|
return Color::rgba(0, 0, 0, 0);
|
|
}
|
|
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();
|
|
for y in 0..height {
|
|
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()) {
|
|
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));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
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 {
|
|
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 {
|
|
fn eq(&self, other: &Self) -> bool {
|
|
self.id == other.id
|
|
}
|
|
}
|