use crate::render::Scene;
use crate::style::Style;
use crate::svg::to_svg;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RasterError {
BadSvg(String),
BadSize {
width: u32,
height: u32,
},
Encode(String),
}
impl std::fmt::Display for RasterError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RasterError::BadSvg(e) => write!(f, "生成的 SVG 解析不了(这是本库的缺陷):{e}"),
RasterError::BadSize { width, height } => write!(f, "画布尺寸不合法:{width}×{height}"),
RasterError::Encode(e) => write!(f, "编码失败:{e}"),
}
}
}
impl std::error::Error for RasterError {}
pub struct Pixels {
pub width: u32,
pub height: u32,
pub rgba: Vec<u8>,
}
pub fn rasterize(scene: &Scene, style: &Style, scale: f32) -> Result<Pixels, RasterError> {
let w = (scene.width as f32 * scale).round();
let h = (scene.height as f32 * scale).round();
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let (wu, hu) = (w.max(0.0) as u32, h.max(0.0) as u32);
if wu == 0 || hu == 0 || wu > 20_000 || hu > 20_000 {
return Err(RasterError::BadSize {
width: wu,
height: hu,
});
}
let svg = to_svg(scene, style);
let mut opt = resvg::usvg::Options::default();
opt.fontdb_mut().load_system_fonts();
let tree =
resvg::usvg::Tree::from_str(&svg, &opt).map_err(|e| RasterError::BadSvg(e.to_string()))?;
let mut pixmap = resvg::tiny_skia::Pixmap::new(wu, hu).ok_or(RasterError::BadSize {
width: wu,
height: hu,
})?;
resvg::render(
&tree,
resvg::tiny_skia::Transform::from_scale(scale, scale),
&mut pixmap.as_mut(),
);
Ok(Pixels {
width: wu,
height: hu,
rgba: pixmap.take(),
})
}
pub fn to_png(scene: &Scene, style: &Style, scale: f32) -> Result<Vec<u8>, RasterError> {
let px = rasterize(scene, style, scale)?;
let pixmap = resvg::tiny_skia::Pixmap::from_vec(
px.rgba,
resvg::tiny_skia::IntSize::from_wh(px.width, px.height).ok_or(RasterError::BadSize {
width: px.width,
height: px.height,
})?,
)
.ok_or(RasterError::BadSize {
width: px.width,
height: px.height,
})?;
pixmap
.encode_png()
.map_err(|e| RasterError::Encode(e.to_string()))
}
pub fn to_jpeg(
scene: &Scene,
style: &Style,
scale: f32,
quality: u8,
) -> Result<Vec<u8>, RasterError> {
let px = rasterize(scene, style, scale)?;
let mut rgb = Vec::with_capacity(px.rgba.len() / 4 * 3);
for p in px.rgba.chunks_exact(4) {
let a = f32::from(p[3]) / 255.0;
for c in &p[..3] {
let v = f32::from(*c) + 255.0 * (1.0 - a);
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
rgb.push(v.clamp(0.0, 255.0) as u8);
}
}
let mut out = Vec::new();
let enc = jpeg_encoder::Encoder::new(&mut out, quality.clamp(1, 100));
enc.encode(
&rgb,
u16::try_from(px.width).map_err(|_| RasterError::BadSize {
width: px.width,
height: px.height,
})?,
u16::try_from(px.height).map_err(|_| RasterError::BadSize {
width: px.width,
height: px.height,
})?,
jpeg_encoder::ColorType::Rgb,
)
.map_err(|e| RasterError::Encode(e.to_string()))?;
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{generate, render::scene};
use omgkit_core::MolBuilder;
fn prep(smi: &str) -> MolBuilder {
let mut m = omgkit_io::smiles::parse(smi).unwrap();
omgkit_chem::pipeline::sanitize(&mut m).unwrap();
m
}
fn sc(smi: &str, style: &Style) -> Scene {
let m = prep(smi);
scene(&m, &generate(&m, style), style)
}
#[test]
fn png_has_the_right_magic_and_size() {
let s = sc("CC(=O)Oc1ccccc1C(=O)O", &Style::ACS_1996);
let png = to_png(&s, &Style::ACS_1996, 300.0 / 72.0).expect("出 PNG");
assert_eq!(&png[..8], b"\x89PNG\r\n\x1a\n", "PNG 头不对");
assert!(png.len() > 1000, "PNG 只有 {} 字节,大概率是空图", png.len());
}
#[test]
fn jpeg_has_the_right_magic() {
let s = sc("c1ccccc1", &Style::ACS_1996);
let j = to_jpeg(&s, &Style::ACS_1996, 2.0, 90).expect("出 JPEG");
assert_eq!(&j[..2], b"\xff\xd8", "JPEG 头不对");
assert_eq!(&j[j.len() - 2..], b"\xff\xd9", "JPEG 尾不对");
}
#[test]
fn the_image_is_not_blank() {
let s = sc("c1ccc2ccccc2c1", &Style::ACS_1996);
let px = rasterize(&s, &Style::ACS_1996, 2.0).expect("光栅化");
let inked = px
.rgba
.chunks_exact(4)
.filter(|p| p[0] < 200 || p[1] < 200 || p[2] < 200)
.count();
assert!(inked > 200, "只有 {inked} 个非白像素,这张图基本是空的");
}
#[test]
fn jpeg_is_not_rendered_on_a_black_background() {
let s = sc("CCO", &Style::ACS_1996);
let j = to_jpeg(&s, &Style::ACS_1996, 2.0, 95).expect("出 JPEG");
let png = to_png(&s, &Style::ACS_1996, 2.0).expect("出 PNG");
assert!(!j.is_empty() && !png.is_empty());
let px = rasterize(&s, &Style::ACS_1996, 2.0).expect("光栅化");
let corner = &px.rgba[..4];
assert!(
corner[3] == 0 || (corner[0] > 200 && corner[1] > 200 && corner[2] > 200),
"左上角是 {corner:?} —— 既不透明也不白,底色错了"
);
}
#[test]
fn the_two_styles_give_different_pixel_sizes() {
let a = rasterize(
&sc("c1ccc2ccccc2c1", &Style::ACS_1996),
&Style::ACS_1996,
2.0,
)
.unwrap();
let c = rasterize(
&sc("c1ccc2ccccc2c1", &Style::CHEMDRAW_DEFAULT),
&Style::CHEMDRAW_DEFAULT,
2.0,
)
.unwrap();
assert!(
c.width > a.width * 3 / 2,
"ChemDraw 默认的键长是 ACS 的 2.08 倍,像素宽却只有 {} vs {}",
c.width,
a.width
);
}
#[test]
fn a_degenerate_size_is_refused_instead_of_panicking() {
let s = sc("CCO", &Style::ACS_1996);
assert!(matches!(
rasterize(&s, &Style::ACS_1996, 0.0),
Err(RasterError::BadSize { .. })
));
assert!(matches!(
rasterize(&s, &Style::ACS_1996, 1e6),
Err(RasterError::BadSize { .. })
));
}
}