use cosmic_text::{SwashContent, SwashImage};
use image::codecs::png::{CompressionType, FilterType};
use image::{ExtendedColorType, ImageEncoder};
use tiny_skia::{FillRule, Paint, PathBuilder, Pixmap, PremultipliedColorU8, Rect, Transform};
use crate::error::{Error, Result};
use crate::layout::{DisplayItem, Layout, PlacedGlyph};
use crate::model::Color;
use crate::theme::{OutputFormat, RenderOptions};
pub(crate) fn paint(layout: &Layout, opts: &RenderOptions) -> Result<Vec<u8>> {
let pix = render_pixmap(layout, opts)?;
encode(&pix, opts.format)
}
fn encode(pix: &Pixmap, format: OutputFormat) -> Result<Vec<u8>> {
let enc = |e: image::ImageError| Error::Encode(e.to_string());
let (w, h) = (pix.width(), pix.height());
match format {
OutputFormat::Png => pix.encode_png().map_err(|e| Error::Encode(e.to_string())),
OutputFormat::PngFast => {
let rgba = pixmap_to_rgba(pix);
let mut buf = Vec::new();
image::codecs::png::PngEncoder::new_with_quality(
&mut buf,
CompressionType::Fast,
FilterType::Adaptive,
)
.write_image(&rgba, w, h, ExtendedColorType::Rgba8)
.map_err(enc)?;
Ok(buf)
}
OutputFormat::Webp | OutputFormat::WebpOrPng => {
if w > 16383 || h > 16383 {
return if format == OutputFormat::WebpOrPng {
pix.encode_png().map_err(|e| Error::Encode(e.to_string()))
} else {
Err(Error::Encode(format!(
"WebP 单边上限 16383px,当前 {w}×{h} 超限;改用 PNG(.png() / .webp_or_png())或调小 scale / width"
)))
};
}
let rgba = pixmap_to_rgba(pix);
let mut buf = Vec::new();
image::codecs::webp::WebPEncoder::new_lossless(&mut buf)
.write_image(&rgba, w, h, ExtendedColorType::Rgba8)
.map_err(enc)?;
Ok(buf)
}
}
}
pub(crate) fn paint_rgba(layout: &Layout, opts: &RenderOptions) -> Result<image::RgbaImage> {
let pix = render_pixmap(layout, opts)?;
let (w, h) = (pix.width(), pix.height());
image::RgbaImage::from_raw(w, h, pixmap_to_rgba(&pix))
.ok_or_else(|| Error::Layout("RGBA 缓冲尺寸不符".into()))
}
fn pixmap_to_rgba(pix: &Pixmap) -> Vec<u8> {
let mut out = Vec::with_capacity((pix.width() * pix.height() * 4) as usize);
for p in pix.pixels() {
let a = p.alpha();
if a == 0 {
out.extend_from_slice(&[0, 0, 0, 0]);
} else {
let un = |c: u8| ((c as u32 * 255 + a as u32 / 2) / a as u32).min(255) as u8;
out.extend_from_slice(&[un(p.red()), un(p.green()), un(p.blue()), a]);
}
}
out
}
fn render_pixmap(layout: &Layout, opts: &RenderOptions) -> Result<Pixmap> {
let mut pix = Pixmap::new(layout.width_px, layout.height_px)
.ok_or_else(|| Error::Layout("画布尺寸非法(过大或为 0)".into()))?;
pix.fill(to_skia(opts.theme.background));
for item in &layout.items {
if let DisplayItem::Rect { x, y, w, h, color, radius, over: false } = item {
draw_rect(&mut pix, *x, *y, *w, *h, *color, *radius);
}
}
for item in &layout.items {
if let DisplayItem::Image { x, y, w, h, src } = item {
if let Some(img) = layout.images.get(*src) {
draw_image(&mut pix, img, *x, *y, *w, *h);
}
}
}
for item in &layout.items {
if let DisplayItem::Glyphs(glyphs) = item {
draw_glyphs(&mut pix, opts, glyphs);
}
}
for item in &layout.items {
if let DisplayItem::Rect { x, y, w, h, color, radius, over: true } = item {
draw_rect(&mut pix, *x, *y, *w, *h, *color, *radius);
}
}
Ok(pix)
}
fn draw_rect(pix: &mut Pixmap, x: f32, y: f32, w: f32, h: f32, color: Color, radius: f32) {
if w <= 0.0 || h <= 0.0 {
return;
}
let mut paint = Paint::default();
paint.set_color_rgba8(color.r, color.g, color.b, color.a);
paint.anti_alias = true;
let id = Transform::identity();
if radius > 0.0 {
if let Some(path) = rounded_rect(x, y, w, h, radius) {
pix.fill_path(&path, &paint, FillRule::Winding, id, None);
}
} else if let Some(rect) = Rect::from_xywh(x, y, w, h) {
pix.fill_rect(rect, &paint, id, None);
}
}
fn rounded_rect(x: f32, y: f32, w: f32, h: f32, r: f32) -> Option<tiny_skia::Path> {
let r = r.min(w / 2.0).min(h / 2.0);
let mut pb = PathBuilder::new();
pb.move_to(x + r, y);
pb.line_to(x + w - r, y);
pb.quad_to(x + w, y, x + w, y + r);
pb.line_to(x + w, y + h - r);
pb.quad_to(x + w, y + h, x + w - r, y + h);
pb.line_to(x + r, y + h);
pb.quad_to(x, y + h, x, y + h - r);
pb.line_to(x, y + r);
pb.quad_to(x, y, x + r, y);
pb.close();
pb.finish()
}
fn draw_glyphs(pix: &mut Pixmap, opts: &RenderOptions, glyphs: &[PlacedGlyph]) {
let pw = pix.width() as i32;
let ph = pix.height() as i32;
let pixels = pix.pixels_mut();
opts.fonts.with_cache(|cache, fs| {
for g in glyphs {
if let Some(img) = cache.get_image(fs, g.cache_key) {
blit_glyph(pixels, pw, ph, img, g.x, g.y, g.color);
}
}
});
}
fn blit_glyph(
pixels: &mut [PremultipliedColorU8],
pw: i32,
ph: i32,
img: &SwashImage,
pen_x: i32,
base_y: i32,
color: Color,
) {
let ox = pen_x.saturating_add(img.placement.left);
let oy = base_y.saturating_sub(img.placement.top);
let gw = img.placement.width as i32;
let gh = img.placement.height as i32;
for j in 0..gh {
let py = oy.saturating_add(j);
if py < 0 || py >= ph {
continue;
}
for i in 0..gw {
let px = ox.saturating_add(i);
if px < 0 || px >= pw {
continue;
}
let dst = &mut pixels[py as usize * pw as usize + px as usize];
match img.content {
SwashContent::Mask => {
let cov = img.data[(j * gw + i) as usize];
blend(dst, color.r, color.g, color.b, color.a, cov);
}
SwashContent::SubpixelMask => {
let k = ((j * gw + i) * 3) as usize;
let cov = ((img.data[k] as u16 + img.data[k + 1] as u16 + img.data[k + 2] as u16)
/ 3) as u8;
blend(dst, color.r, color.g, color.b, color.a, cov);
}
SwashContent::Color => {
let k = ((j * gw + i) * 4) as usize;
let (r, g, b, a) =
(img.data[k], img.data[k + 1], img.data[k + 2], img.data[k + 3]);
blend(dst, r, g, b, 255, a);
}
}
}
}
}
fn blend(dst: &mut PremultipliedColorU8, r: u8, g: u8, b: u8, a_color: u8, cov: u8) {
let sa = (cov as f32 / 255.0) * (a_color as f32 / 255.0);
if sa <= 0.0 {
return;
}
let inv = 1.0 - sa;
let (dr, dg, db, da) =
(dst.red() as f32, dst.green() as f32, dst.blue() as f32, dst.alpha() as f32);
let nr = (r as f32 * sa + dr * inv).round() as u8;
let ng = (g as f32 * sa + dg * inv).round() as u8;
let nb = (b as f32 * sa + db * inv).round() as u8;
let na = (255.0 * sa + da * inv).round() as u8;
if let Some(c) = PremultipliedColorU8::from_rgba(nr, ng, nb, na) {
*dst = c;
}
}
fn draw_image(pix: &mut Pixmap, img: &image::RgbaImage, x: f32, y: f32, w: f32, h: f32) {
if w <= 0.0 || h <= 0.0 {
return;
}
let (dw, dh) = ((w.round() as u32).max(1), (h.round() as u32).max(1));
let resized;
let src = if (dw, dh) == (img.width(), img.height()) {
img
} else {
let filter = if dw < img.width() || dh < img.height() {
image::imageops::FilterType::Lanczos3
} else {
image::imageops::FilterType::CatmullRom
};
resized = image::imageops::resize(img, dw, dh, filter);
&resized
};
let (pw, ph) = (pix.width() as i32, pix.height() as i32);
let (ox, oy) = (x.round() as i32, y.round() as i32);
let pixels = pix.pixels_mut();
for j in 0..dh as i32 {
let py = oy.saturating_add(j);
if py < 0 || py >= ph {
continue;
}
for i in 0..dw as i32 {
let px = ox.saturating_add(i);
if px < 0 || px >= pw {
continue;
}
let p = src.get_pixel(i as u32, j as u32).0;
blend(&mut pixels[py as usize * pw as usize + px as usize], p[0], p[1], p[2], 255, p[3]);
}
}
}
fn to_skia(c: Color) -> tiny_skia::Color {
tiny_skia::Color::from_rgba8(c.r, c.g, c.b, c.a)
}