use crate::layout::Layout;
use crate::svg::RenderOptions;
use chematic_core::Molecule;
use tiny_skia::{Color, Paint, Pixmap, Stroke};
const PIXELS_PER_UNIT: f64 = 10.0;
const BOND_WIDTH: f32 = 1.5;
const ATOM_RADIUS: f32 = 3.0;
fn cpk_color(atomic_number: u8) -> (u8, u8, u8) {
match atomic_number {
1 => (255, 255, 255), 6 => (80, 80, 80), 7 => (48, 80, 248), 8 => (255, 13, 13), 9 => (144, 224, 80), 15 => (255, 128, 0), 16 => (255, 200, 50), 17 => (31, 240, 31), 35 => (166, 41, 41), 53 => (148, 0, 148), _ => (255, 20, 147), }
}
fn get_char_bitmap(ch: char) -> Option<[u8; 5]> {
match ch.to_ascii_uppercase() {
'C' => Some([0b0110, 0b1001, 0b1000, 0b1001, 0b0110]),
'N' => Some([0b1001, 0b1011, 0b1101, 0b1001, 0b1001]),
'O' => Some([0b0110, 0b1001, 0b1001, 0b1001, 0b0110]),
'H' => Some([0b1001, 0b1001, 0b1111, 0b1001, 0b1001]),
'F' => Some([0b1111, 0b1000, 0b1110, 0b1000, 0b1000]),
'P' => Some([0b1110, 0b1001, 0b1110, 0b1000, 0b1000]),
'S' => Some([0b0110, 0b1000, 0b0110, 0b0001, 0b1110]),
'B' => Some([0b1110, 0b1001, 0b1110, 0b1001, 0b1110]),
'I' => Some([0b1111, 0b0100, 0b0100, 0b0100, 0b1111]),
'K' => Some([0b1001, 0b1010, 0b1100, 0b1010, 0b1001]),
'X' => Some([0b1001, 0b0110, 0b0110, 0b0110, 0b1001]),
_ => None,
}
}
pub fn render_png(mol: &Molecule, layout: &Layout) -> Vec<u8> {
render_png_opts(mol, layout, &RenderOptions::default())
}
pub fn render_png_opts(mol: &Molecule, layout: &Layout, _opts: &RenderOptions) -> Vec<u8> {
if mol.atom_count() == 0 {
return empty_png();
}
let bounds = layout.bounding_box();
let width_units = bounds.2 - bounds.0 + 2.0;
let height_units = bounds.3 - bounds.1 + 2.0;
let width = (width_units * PIXELS_PER_UNIT).max(100.0) as u32;
let height = (height_units * PIXELS_PER_UNIT).max(100.0) as u32;
let mut pixmap = match Pixmap::new(width, height) {
Some(pm) => pm,
None => return empty_png(),
};
pixmap.fill(Color::WHITE);
let offset_x = -bounds.0 + 1.0;
let offset_y = -bounds.1 + 1.0;
let mut paint_bond = Paint::default();
paint_bond.set_color(Color::BLACK);
for (idx, _) in mol.atoms() {
let p = layout.get(idx);
let x = ((p.x + offset_x) * PIXELS_PER_UNIT) as f32;
let y = ((p.y + offset_y) * PIXELS_PER_UNIT) as f32;
let r = ATOM_RADIUS;
if let Some(rect) = tiny_skia::Rect::from_xywh(x - r, y - r, 2.0 * r, 2.0 * r) {
let atom = mol.atom(idx);
let (r_val, g_val, b_val) = cpk_color(atom.element.atomic_number());
let mut paint = Paint::default();
paint.set_color(Color::from_rgba8(r_val, g_val, b_val, 255));
pixmap.fill_rect(rect, &paint, tiny_skia::Transform::default(), None);
}
let atom = mol.atom(idx);
let symbol = atom.element.symbol();
for (i, ch) in symbol.chars().enumerate() {
if let Some(bitmap) = get_char_bitmap(ch) {
let label_x = x - 2.0 + (i as f32 * 3.0);
let label_y = y - 2.5;
let mut text_paint = Paint::default();
text_paint.set_color(Color::BLACK);
for (row, bits) in bitmap.iter().enumerate() {
for col in 0..4 {
if (bits >> (3 - col)) & 1 == 1 {
let px = label_x + (col as f32 * 0.5);
let py = label_y + (row as f32 * 0.5);
if let Some(rect) = tiny_skia::Rect::from_xywh(px, py, 0.5, 0.5) {
pixmap.fill_rect(
rect,
&text_paint,
tiny_skia::Transform::default(),
None,
);
}
}
}
}
}
}
}
use chematic_core::BondOrder;
for (_, bond) in mol.bonds() {
let p1 = layout.get(bond.atom1);
let p2 = layout.get(bond.atom2);
let x1 = ((p1.x + offset_x) * PIXELS_PER_UNIT) as f32;
let y1 = ((p1.y + offset_y) * PIXELS_PER_UNIT) as f32;
let x2 = ((p2.x + offset_x) * PIXELS_PER_UNIT) as f32;
let y2 = ((p2.y + offset_y) * PIXELS_PER_UNIT) as f32;
let stroke_width = match bond.order {
BondOrder::Up => BOND_WIDTH * 2.5, BondOrder::Down => BOND_WIDTH * 0.8, _ => BOND_WIDTH, };
let stroke = Stroke {
width: stroke_width,
..Default::default()
};
let mut pb = tiny_skia::PathBuilder::new();
pb.move_to(x1, y1);
pb.line_to(x2, y2);
if let Some(path) = pb.finish() {
pixmap.stroke_path(
&path,
&paint_bond,
&stroke,
tiny_skia::Transform::default(),
None,
);
}
if bond.order == BondOrder::Down {
let dx = x2 - x1;
let dy = y2 - y1;
let len = (dx * dx + dy * dy).sqrt();
if len > 0.0 {
let ux = dx / len;
let uy = dy / len;
let dash_len = 2.0;
let gap_len = 2.0;
let mut pos = 0.0;
while pos < len {
let start_pos = (pos).min(len);
let end_pos = (pos + dash_len).min(len);
let sx = x1 + ux * start_pos;
let sy = y1 + uy * start_pos;
let ex = x1 + ux * end_pos;
let ey = y1 + uy * end_pos;
let mut pb = tiny_skia::PathBuilder::new();
pb.move_to(sx, sy);
pb.line_to(ex, ey);
if let Some(path) = pb.finish() {
pixmap.stroke_path(
&path,
&paint_bond,
&stroke,
tiny_skia::Transform::default(),
None,
);
}
pos += dash_len + gap_len;
}
}
}
}
pixmap.encode_png().unwrap_or_else(|_| empty_png())
}
fn empty_png() -> Vec<u8> {
vec![
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44,
0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x02, 0x00, 0x00, 0x00, 0x90,
0x77, 0x53, 0xDE, 0x00, 0x00, 0x00, 0x0C, 0x49, 0x44, 0x41, 0x54, 0x08, 0x99, 0x01, 0x01,
0x00, 0x00, 0xFE, 0xFF, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, 0x9A, 0x7E, 0x0B, 0xBB, 0x00,
0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82,
]
}