use super::{FFramesRendererError, FFramesRendererResult, RgbaFrame, sheet};
use crate::usvgr;
use serde::Serialize;
use std::fmt::Write as _;
use usvgr::tiny_skia_path::PathSegment;
const MAX_LABELS: usize = 400;
#[derive(Debug, Clone, Serialize)]
pub struct LabelledElement {
pub id: String,
pub kind: &'static str,
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
#[serde(skip)]
outline: Option<String>,
}
fn path_data(path: &usvgr::tiny_skia_path::Path, transform: usvgr::Transform) -> Option<String> {
let path = path.clone().transform(transform)?;
let mut data = String::new();
for segment in path.segments() {
let _ = match segment {
PathSegment::MoveTo(p) => write!(data, "M{} {} ", p.x, p.y),
PathSegment::LineTo(p) => write!(data, "L{} {} ", p.x, p.y),
PathSegment::QuadTo(a, p) => write!(data, "Q{} {} {} {} ", a.x, a.y, p.x, p.y),
PathSegment::CubicTo(a, b, p) => {
write!(data, "C{} {} {} {} {} {} ", a.x, a.y, b.x, b.y, p.x, p.y)
}
PathSegment::Close => write!(data, "Z "),
};
}
Some(data)
}
fn collect(group: &usvgr::Group, out: &mut Vec<LabelledElement>) {
for node in group.children() {
if let usvgr::Node::Group(child) = node
&& child.opacity().get() == 0.
{
continue;
}
let id = node.id();
if !id.is_empty() {
let bbox = node.abs_bounding_box();
if bbox.width().is_finite() && bbox.height().is_finite() {
let (kind, outline) = match node {
usvgr::Node::Path(path) => {
("path", path_data(path.data(), path.abs_transform()))
}
usvgr::Node::FastShape(shape) => (
"path",
shape
.to_path()
.and_then(|path| path_data(path.data(), path.abs_transform())),
),
usvgr::Node::Group(_) => ("group", None),
usvgr::Node::Image(_) => ("image", None),
usvgr::Node::Text(_) => ("text", None),
};
out.push(LabelledElement {
id: id.to_owned(),
kind,
x: bbox.x(),
y: bbox.y(),
width: bbox.width(),
height: bbox.height(),
outline,
});
}
}
if let usvgr::Node::Group(child) = node {
collect(child, out);
}
}
}
pub fn labelled_elements(tree: &usvgr::Tree) -> Vec<LabelledElement> {
let mut elements = Vec::new();
collect(tree.root(), &mut elements);
elements
}
fn escape_xml(text: &str) -> String {
text.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
}
pub fn draw_labels(
frame: &RgbaFrame,
tree: &usvgr::Tree,
elements: &[LabelledElement],
fontdb: &usvgr::fontdb::Database,
font_family: &str,
) -> FFramesRendererResult<RgbaFrame> {
let system;
let fontdb = if fontdb.faces().next().is_some() {
fontdb
} else {
let mut db = usvgr::fontdb::Database::new();
db.load_system_fonts();
system = db;
&system
};
let size = tree.size();
let unit = size.width() / frame.width as f32;
let font_size = 12. * unit;
let family = escape_xml(&sheet::label_font_family(fontdb, font_family));
let mut shapes = String::new();
let mut labels = String::new();
for element in elements.iter().take(MAX_LABELS) {
let colour = if element.kind == "path" {
"#ff2bd6"
} else {
"#ffd400"
};
match &element.outline {
Some(outline) => {
let _ = write!(
shapes,
r#"<path d="{outline}" fill="none" stroke="{colour}" stroke-width="{}" />"#,
1.2 * unit
);
}
None => {
let _ = write!(
shapes,
r#"<rect x="{}" y="{}" width="{}" height="{}" fill="none" stroke="{colour}" stroke-width="{}" stroke-dasharray="{} {}" />"#,
element.x,
element.y,
element.width,
element.height,
1.2 * unit,
6. * unit,
4. * unit
);
}
}
let (x, y) = if element.kind == "path" {
(
element.x + element.width / 2.,
element.y + element.height / 2.,
)
} else {
(element.x + 2. * unit, element.y + font_size)
};
let text = escape_xml(&element.id);
let width = text.chars().count() as f32 * font_size * 0.6 + 6. * unit;
let anchor_x = if element.kind == "path" {
x - width / 2.
} else {
x
};
let _ = write!(
labels,
r##"<rect x="{}" y="{}" width="{width}" height="{}" rx="{}" fill="#000000" fill-opacity="0.7" /><text x="{}" y="{}" font-size="{font_size}" font-family="{family}" fill="#ffffff">{text}</text>"##,
anchor_x,
y - font_size * 0.95,
font_size * 1.3,
2. * unit,
anchor_x + 3. * unit,
y,
);
}
let svg = format!(
r#"<svg xmlns="http://www.w3.org/2000/svg" width="{}" height="{}" viewBox="0 0 {} {}">{shapes}{labels}</svg>"#,
frame.width,
frame.height,
size.width(),
size.height()
);
let overlay = usvgr::Tree::from_str(&svg, &usvgr::Options::default(), fontdb)
.map_err(|err| FFramesRendererError::Internal(format!("element labels: {err}")))?;
let mut pixmap = sheet::to_pixmap(frame)?;
let ctx = svgr::Context::new_from_pixmap_unsafe(&pixmap);
svgr::render(
&overlay,
super::fit_transform(&overlay, frame.width, frame.height),
&mut pixmap.as_mut(),
&mut svgr::SvgrCache::none(),
&svgr::PixmapPool::new(),
&ctx,
);
Ok(RgbaFrame::from_premultiplied(
frame.width,
frame.height,
pixmap.take(),
))
}