use std::{
fmt::{self, Display},
fs,
path::Path,
};
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
#[cfg(any(feature = "png", feature = "pdf"))]
use include_dir::{Dir, include_dir};
#[cfg(any(feature = "png", feature = "pdf"))]
use resvg::usvg::Tree;
mod config;
mod edge;
mod multigraph;
mod node;
mod style;
mod subgraph;
const TYPST_PREFIX: &str = "__typst__";
#[cfg(feature = "png")]
const SCALING_FACTOR: f32 = 8.0;
#[cfg(feature = "pdf")]
const DPI: f32 = 400.0;
#[cfg(any(feature = "png", feature = "pdf"))]
static FONTS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/fonts");
pub mod extras;
pub use config::*;
pub use edge::{ArrowKind, Edge};
pub use multigraph::Multigraph;
pub use node::{Node, NodeShape};
pub use style::*;
pub use subgraph::Subgraph;
pub type EdgeId = usize;
pub type NodeId = usize;
pub type SubgraphId = usize;
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize)]
pub enum Port {
West,
NorthWest,
North,
NorthEast,
East,
SouthEast,
South,
SouthWest,
Center,
}
#[derive(Clone, Debug)]
pub struct PlotSVG {
header: String,
defs: String,
content: String,
style: String,
}
impl PlotSVG {
pub fn from(content: String, width: f32, height: f32, style: &Style) -> Self {
let header = format!("<svg viewBox=\"0 0 {width} {height}\" xmlns=\"http://www.w3.org/2000/svg\">");
let (mut defs, mut css_style) = (String::new(), String::new());
for section in style.get_utils().get_defs() {
defs.push_str(section);
}
for webfont_url in style.get_utils().get_webfonts() {
css_style += &format!("@import url(\"{webfont_url}\");\n");
}
if style.get_fullscreen() {
css_style += &format!("svg {{ background-color: {}; }}\n", style.get_background_color());
}
Self {
content,
defs,
header,
style: css_style,
}
}
}
impl PlotSVG {
pub fn to_complete_svg(&self) -> String {
format!(
"{header}\n<defs>{defs}</defs>\n{content}\n<style>\n{style}</style>\n</svg>",
header = self.header,
defs = self.defs,
content = self.content,
style = self.style
)
}
pub fn save_svg<F: Display>(&self, filename: F) -> Result<()> {
let mut filepath = filename.to_string();
if !filepath.ends_with(".svg") {
filepath += ".svg";
}
let path = Path::new(&filepath);
if let Some(parent_dir) = path.parent() {
fs::create_dir_all(parent_dir)?;
}
let svg = self.to_complete_svg();
std::fs::write(filepath, svg).context("Error saving Graphplot to file")
}
#[cfg(not(feature = "pdf"))]
pub fn save_pdf<F: Display>(&self, _filename: F) -> Result<()> {
anyhow::bail!("Feature 'pdf' is disabled");
}
#[cfg(feature = "pdf")]
pub fn save_pdf<F: Display>(&self, filename: F) -> Result<()> {
use svg2pdf::{ConversionOptions, PageOptions};
let mut filepath = filename.to_string();
let tree = self.create_tree().context("Error creating pixmap")?;
let mut page_opt = PageOptions::default();
page_opt.dpi = DPI;
let pdf_data = svg2pdf::to_pdf(&tree, ConversionOptions::default(), page_opt).expect("Error creating PDF");
if !filepath.ends_with(".pdf") {
filepath += ".pdf";
}
let path = Path::new(&filepath);
if let Some(parent_dir) = path.parent() {
fs::create_dir_all(parent_dir)?;
}
std::fs::write(filepath, pdf_data).context("Error saving Graphplot PDF to file")
}
#[cfg(not(feature = "png"))]
pub fn save_png<F: Display>(&self, _filename: F) -> Result<()> {
anyhow::bail!("Feature 'png' is disabled");
}
#[cfg(feature = "png")]
pub fn save_png<F: Display>(&self, filename: F) -> Result<()> {
use resvg::{tiny_skia::Pixmap, usvg::Transform};
let mut filepath = filename.to_string();
let tree = self.create_tree().context("Error creating pixmap")?;
let size = tree.size();
let width = (SCALING_FACTOR * size.width()).ceil() as u32;
let height = (SCALING_FACTOR * size.height()).ceil() as u32;
let mut pixmap = Pixmap::new(width, height).context("Error allocating pixmap")?;
resvg::render(&tree, Transform::from_scale(SCALING_FACTOR, SCALING_FACTOR), &mut pixmap.as_mut());
let png_data = pixmap.encode_png().context("Error rendering pixmap to png")?;
if !filepath.ends_with(".png") {
filepath += ".png";
}
let path = Path::new(&filepath);
if let Some(parent_dir) = path.parent() {
fs::create_dir_all(parent_dir)?;
}
std::fs::write(filepath, png_data).context("Error saving Graphplot PNG to file")
}
#[cfg(any(feature = "png", feature = "pdf"))]
fn create_tree(&self) -> Result<Tree> {
use resvg::usvg::{self, Options, Tree};
use std::sync::Arc;
let mut fontdb = usvg::fontdb::Database::new();
let svg = self.to_complete_svg();
let mut opt = Options::default();
Self::load_embedded_fonts(&mut fontdb);
opt.fontdb = Arc::new(fontdb);
let tree = Tree::from_data(svg.as_bytes(), &opt)?;
Ok(tree)
}
#[cfg(any(feature = "png", feature = "pdf"))]
fn load_embedded_fonts(db: &mut resvg::usvg::fontdb::Database) {
fn load_recursive(dir: &Dir, db: &mut resvg::usvg::fontdb::Database) {
for file in dir.files() {
db.load_font_data(file.contents().to_vec());
}
for subdir in dir.dirs() {
load_recursive(subdir, db);
}
}
load_recursive(&FONTS_DIR, db);
}
}
impl fmt::Display for PlotSVG {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.content)
}
}