pub mod avatar;
pub mod avatar_cache;
pub mod avatar_options;
pub mod core;
pub mod data;
pub mod render;
pub mod svg;
pub mod util;
use std::io;
use std::path::Path;
use std::sync::{Arc, OnceLock};
use avatar::Avatar;
use avatar_cache::{AvatarCache, CacheKey};
use avatar_options::AvatarOptions;
use core::svg_composer::SvgComposer;
use render::AvatarFormat;
use render::Renderers;
use tiny_skia::Pixmap;
pub struct Multiavatar;
fn composer() -> &'static SvgComposer {
static COMPOSER: OnceLock<SvgComposer> = OnceLock::new();
COMPOSER.get_or_init(|| SvgComposer::with_view_box("0 0 231 231"))
}
pub fn multiavatar(input: &str) -> String {
multiavatar_full(input, false, None, None)
}
pub fn multiavatar_with(input: &str, sans_env: bool) -> String {
multiavatar_full(input, sans_env, None, None)
}
pub fn multiavatar_full(
input: &str,
sans_env: bool,
part: Option<&str>,
theme: Option<&str>,
) -> String {
let options = AvatarOptions::builder().sans_env(sans_env).build();
let options = match part {
Some(p) => options_part(&options, p),
None => options,
};
let options = match theme {
Some(t) => AvatarOptions::builder()
.sans_env(options.sans_env())
.part_owned(options.part().map(str::to_string))
.theme(t)
.build(),
None => options,
};
avatar_with(input, &options).svg().to_string()
}
pub fn avatar(input: &str) -> Arc<Avatar> {
avatar_with(input, &AvatarOptions::defaults())
}
pub fn avatar_with(input: &str, options: &AvatarOptions) -> Arc<Avatar> {
let key = CacheKey::new(
input,
options.sans_env(),
options.part(),
options.theme(),
options.svg_width(),
options.svg_height(),
options.optimize_svg(),
);
if let Some(hit) = AvatarCache::get(&key) {
return hit;
}
let mut svg = composer().compose(
input,
options.sans_env(),
options.part(),
options.theme(),
);
if options.optimize_svg() {
svg = crate::core::svg_optimizer::optimize(&svg);
}
let avatar = Arc::new(Avatar::new(input.to_string(), options.clone(), svg));
AvatarCache::put(key, avatar.clone());
avatar
}
pub fn cache_size() -> usize {
AvatarCache::size()
}
pub fn clear_cache() {
AvatarCache::clear();
}
pub fn to_image(input: &str) -> Option<Pixmap> {
to_image_sized(input, AvatarOptions::defaults().size())
}
pub fn to_image_sized(input: &str, size: u32) -> Option<Pixmap> {
let options = AvatarOptions::builder().size(size).build();
to_image_with(input, &options)
}
pub fn to_image_with(input: &str, options: &AvatarOptions) -> Option<Pixmap> {
avatar_with(input, options).to_image_sized(options.size())
}
pub fn to_png(input: &str) -> Vec<u8> {
to_png_sized(input, AvatarOptions::defaults().size())
}
pub fn to_png_sized(input: &str, size: u32) -> Vec<u8> {
let options = AvatarOptions::builder().size(size).build();
to_png_with(input, &options)
}
pub fn to_png_with(input: &str, options: &AvatarOptions) -> Vec<u8> {
avatar_with(input, options).png_sized(options.size())
}
pub fn to_image_smooth(input: &str, size: u32) -> Option<Pixmap> {
to_image_smooth_with(input, size, 4)
}
pub fn to_image_smooth_with(input: &str, size: u32, factor: u32) -> Option<Pixmap> {
if size == 0 || factor <= 1 {
return to_image_sized(input, size);
}
let big = to_image_sized(input, size * factor)?;
render::downscale::bilinear(&big, size)
}
pub fn to_png_smooth(input: &str) -> Vec<u8> {
to_png_smooth_sized(input, AvatarOptions::defaults().size())
}
pub fn to_png_smooth_sized(input: &str, size: u32) -> Vec<u8> {
to_image_smooth(input, size)
.map(|p| render::png_writer::to_png(&p))
.unwrap_or_default()
}
pub fn to_jpg_smooth(input: &str) -> Vec<u8> {
to_jpg_smooth_sized(input, AvatarOptions::defaults().size())
}
pub fn to_jpg_smooth_sized(input: &str, size: u32) -> Vec<u8> {
let Some(pixmap) = to_image_smooth(input, size) else {
return Vec::new();
};
let data = pixmap.data();
let mut rgb = Vec::with_capacity((size * size) as usize * 3);
for px in data.chunks(4) {
let a = px[3];
if a == 255 {
rgb.extend_from_slice(&px[..3]);
} else {
rgb.push(((px[0] as u32 + 255 * (255 - a as u32)).min(255)) as u8);
rgb.push(((px[1] as u32 + 255 * (255 - a as u32)).min(255)) as u8);
rgb.push(((px[2] as u32 + 255 * (255 - a as u32)).min(255)) as u8);
}
}
render::jpg_writer::to_jpg_rgb(&rgb, size, size)
}
pub fn to_jpg(input: &str) -> Vec<u8> {
to_jpg_sized(input, AvatarOptions::defaults().size())
}
pub fn to_jpg_sized(input: &str, size: u32) -> Vec<u8> {
let options = AvatarOptions::builder().size(size).build();
to_jpg_with(input, &options)
}
pub fn to_jpg_with(input: &str, options: &AvatarOptions) -> Vec<u8> {
avatar_with(input, options).jpg_sized(options.size())
}
pub fn to_gif(input: &str) -> Vec<u8> {
to_gif_sized(input, AvatarOptions::defaults().size())
}
pub fn to_gif_sized(input: &str, size: u32) -> Vec<u8> {
let options = AvatarOptions::builder().size(size).build();
to_gif_with(input, &options)
}
pub fn to_gif_with(input: &str, options: &AvatarOptions) -> Vec<u8> {
let a = avatar_with(input, options);
if a.is_empty() {
return Vec::new();
}
Renderers::create(AvatarFormat::Gif).render(a.svg(), options.size())
}
pub fn to_data_uri(input: &str) -> String {
to_data_uri_sized(input, AvatarOptions::defaults().size())
}
pub fn to_data_uri_sized(input: &str, size: u32) -> String {
let options = AvatarOptions::builder().size(size).build();
to_data_uri_with(input, &options)
}
pub fn to_data_uri_with(input: &str, options: &AvatarOptions) -> String {
avatar_with(input, options).data_uri_sized(options.size())
}
pub fn to_favicon_zip(input: &str) -> Vec<u8> {
to_favicon_zip_sized(input, &[])
}
pub fn to_favicon_zip_sized(input: &str, sizes: &[u32]) -> Vec<u8> {
let svg = multiavatar(input);
if svg.is_empty() {
return Vec::new();
}
render::favicons::to_zip(&svg, sizes)
}
pub fn to_favicon_ico(input: &str) -> Vec<u8> {
to_favicon_ico_sized(input, &[])
}
pub fn to_favicon_ico_sized(input: &str, sizes: &[u32]) -> Vec<u8> {
let svg = multiavatar(input);
if svg.is_empty() {
return Vec::new();
}
render::favicons::to_ico(&svg, sizes)
}
pub fn render(input: &str, format: AvatarFormat, options: &AvatarOptions) -> Vec<u8> {
let a = avatar_with(input, options);
if a.is_empty() {
return Vec::new();
}
match format {
AvatarFormat::Svg => a.svg().as_bytes().to_vec(),
AvatarFormat::Png => Renderers::create(AvatarFormat::Png).render(a.svg(), options.size()),
AvatarFormat::Jpg => Renderers::create(AvatarFormat::Jpg).render(a.svg(), options.size()),
AvatarFormat::Gif => Renderers::create(AvatarFormat::Gif).render(a.svg(), options.size()),
}
}
pub fn write_png(input: &str, path: impl AsRef<Path>) -> io::Result<()> {
write_png_sized(input, AvatarOptions::defaults().size(), path)
}
pub fn write_png_sized(input: &str, size: u32, path: impl AsRef<Path>) -> io::Result<()> {
let bytes = to_png_sized(input, size);
std::fs::write(path, bytes)
}
pub fn write_favicon_zip(input: &str, path: impl AsRef<Path>) -> io::Result<()> {
let bytes = to_favicon_zip(input);
std::fs::write(path, bytes)
}
pub fn write_favicon_ico(input: &str, path: impl AsRef<Path>) -> io::Result<()> {
let bytes = to_favicon_ico(input);
std::fs::write(path, bytes)
}
fn options_part(options: &AvatarOptions, part: &str) -> AvatarOptions {
AvatarOptions::builder()
.sans_env(options.sans_env())
.part(part)
.theme_owned(options.theme().map(str::to_string))
.size(options.size())
.svg_size(options.svg_width(), options.svg_height())
.format(options.format())
.optimize_svg(options.optimize_svg())
.build()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lru_key_ignores_size_and_format() {
clear_cache();
let u = "cache-unit-input";
let _ = to_png_sized(u, 64);
let base = cache_size();
assert!(base >= 1, "首次生成应入缓存");
let _ = to_png_sized(u, 128);
let _ = to_png_sized(u, 512);
let _ = to_gif_sized(u, 256);
let _ = to_jpg_sized(u, 256);
assert_eq!(cache_size(), base, "size/format 不应新增缓存键");
clear_cache();
}
#[test]
fn empty_input_facade() {
assert_eq!(multiavatar(""), "");
assert!(to_png("").is_empty());
assert!(to_jpg("").is_empty());
assert!(to_gif("").is_empty());
assert_eq!(to_data_uri(""), "");
assert!(to_image("").is_none());
assert!(to_favicon_zip("").is_empty());
assert!(to_favicon_ico("").is_empty());
assert!(to_image_smooth("", 256).is_none());
assert!(to_png_smooth("").is_empty());
assert!(to_jpg_smooth("").is_empty());
}
#[test]
fn smooth_ssaa_smaller_and_different() {
let input = "smooth-unit-input";
let direct = to_image_sized(input, 64).unwrap();
let smooth = to_image_smooth(input, 64).unwrap();
assert_eq!(smooth.width(), 64);
assert_eq!(smooth.height(), 64);
assert_ne!(smooth.data(), direct.data(), "SSAA 输出应不同于直接渲染");
let same = to_image_smooth_with(input, 64, 1).unwrap();
assert_eq!(same.data(), direct.data());
assert!(!to_png_smooth_sized(input, 64).is_empty());
assert!(!to_jpg_smooth_sized(input, 64).is_empty());
}
}
#[cfg(target_arch = "wasm32")]
#[allow(non_snake_case)] pub mod wasm {
use wasm_bindgen::prelude::*;
use crate::{
multiavatar, to_data_uri_sized, to_favicon_ico, to_favicon_zip, to_gif_sized,
to_jpg_sized, to_png_sized,
};
#[wasm_bindgen]
pub fn toSvg(input: &str) -> String {
multiavatar(input)
}
#[wasm_bindgen]
pub fn toPng(input: &str, size: u32) -> Vec<u8> {
to_png_sized(input, size)
}
#[wasm_bindgen]
pub fn toJpg(input: &str, size: u32) -> Vec<u8> {
to_jpg_sized(input, size)
}
#[wasm_bindgen]
pub fn toGif(input: &str, size: u32) -> Vec<u8> {
to_gif_sized(input, size)
}
#[wasm_bindgen]
pub fn toDataUri(input: &str, size: u32) -> String {
to_data_uri_sized(input, size)
}
#[wasm_bindgen]
pub fn toFaviconZip(input: &str) -> Vec<u8> {
to_favicon_zip(input)
}
#[wasm_bindgen]
pub fn toFaviconIco(input: &str) -> Vec<u8> {
to_favicon_ico(input)
}
}