typography and documents: formula and diagram SVG for export; a diagram label without a font is an error
This commit is contained in:
5 files changed
+100
-3
No files matched your search
@@ -1,6 +1,6 @@
|
||||
use std::{
|
||||
panic::{AssertUnwindSafe, catch_unwind},
|
||||
sync::Arc,
|
||||
sync::{Arc, OnceLock},
|
||||
};
|
||||
|
||||
use gpui::{
|
||||
@@ -95,6 +95,72 @@ fn svg(chart: &Chart) -> Result<String, SharedString> {
|
||||
}
|
||||
}
|
||||
|
||||
/// `source` as light SVG of plain paths, for export.
|
||||
pub fn diagram_svg(source: &str) -> Result<String, SharedString> {
|
||||
let chart = Chart {
|
||||
source: source.to_owned().into(),
|
||||
dark: false,
|
||||
ground: "#ffffff".into(),
|
||||
ink: "#000000".into(),
|
||||
scale: 100,
|
||||
};
|
||||
let drawn = svg(&chart)?;
|
||||
// Markers and text become paths any reader draws.
|
||||
let options = usvg::Options {
|
||||
fontdb: fonts(),
|
||||
..usvg::Options::default()
|
||||
};
|
||||
let tree = usvg::Tree::from_data(drawn.as_bytes(), &options)
|
||||
.map_err(|error| SharedString::from(format!("{error}")))?;
|
||||
// A label without a font vanishes: say so.
|
||||
let (written, kept) = (labels(&drawn)?, texts(tree.root()));
|
||||
if kept < written {
|
||||
log::error!("diagram: {kept} of {written} labels found a font");
|
||||
return Err("no installed font could set this diagram's labels".into());
|
||||
}
|
||||
Ok(tree.to_string(&usvg::WriteOptions::default()))
|
||||
}
|
||||
|
||||
/// The `<text>` elements holding words.
|
||||
fn labels(svg: &str) -> Result<usize, SharedString> {
|
||||
let doc = usvg::roxmltree::Document::parse(svg)
|
||||
.map_err(|error| SharedString::from(format!("{error}")))?;
|
||||
let worded = |node: &usvg::roxmltree::Node| {
|
||||
node.descendants()
|
||||
.any(|word| word.text().is_some_and(|text| !text.trim().is_empty()))
|
||||
};
|
||||
Ok(doc
|
||||
.descendants()
|
||||
.filter(|node| node.has_tag_name("text") && worded(node))
|
||||
.count())
|
||||
}
|
||||
|
||||
/// Text nodes usvg set.
|
||||
fn texts(group: &usvg::Group) -> usize {
|
||||
group
|
||||
.children()
|
||||
.iter()
|
||||
.map(|node| match node {
|
||||
usvg::Node::Group(inner) => texts(inner),
|
||||
usvg::Node::Text(_) => 1,
|
||||
_ => 0,
|
||||
})
|
||||
.sum()
|
||||
}
|
||||
|
||||
/// The system's fonts, read once.
|
||||
fn fonts() -> Arc<usvg::fontdb::Database> {
|
||||
static FONTS: OnceLock<Arc<usvg::fontdb::Database>> = OnceLock::new();
|
||||
FONTS
|
||||
.get_or_init(|| {
|
||||
let mut fonts = usvg::fontdb::Database::new();
|
||||
fonts.load_system_fonts();
|
||||
log::info!("diagram: {} system font faces for export", fonts.len());
|
||||
Arc::new(fonts)
|
||||
})
|
||||
.clone()
|
||||
}
|
||||
|
||||
/// `#rrggbb` of a theme color.
|
||||
fn hex(color: Hsla) -> SharedString {
|
||||
let rgb = color.to_rgb();
|
||||
@@ -182,6 +248,17 @@ mod tests {
|
||||
assert!(!why.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_exported_diagram_is_plain_paths() {
|
||||
let svg = diagram_svg(
|
||||
"flowchart LR
|
||||
A[Start] --> B",
|
||||
)
|
||||
.expect("draws");
|
||||
assert!(svg.contains("<path"), "{svg}");
|
||||
assert!(!svg.contains("<marker") && !svg.contains("<text"), "{svg}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn colors_write_as_hex() {
|
||||
assert_eq!(hex(gpui::rgb(0x2c2c2c).into()), "#2c2c2c");
|
||||
|
||||
@@ -33,7 +33,7 @@ pub(crate) use blocks::source;
|
||||
pub use blocks::{Align, BlockData, BlockEditor, BlockEvent, BlockKind, Media};
|
||||
pub use contents::outline;
|
||||
pub use counts::{ReadingTime, WordCount};
|
||||
pub use diagram::Diagram;
|
||||
pub use diagram::{Diagram, diagram_svg};
|
||||
pub use document::{
|
||||
Attachment, DocumentEvent, DocumentHeading, DocumentLook, LARGEST_READ, MarkdownDocument,
|
||||
TableEdit, TableRefusal,
|
||||
|
||||
@@ -116,6 +116,18 @@ impl Asset for Typesetter {
|
||||
}
|
||||
}
|
||||
|
||||
/// `source` as black outlined SVG, for export.
|
||||
pub fn formula_svg(source: &str, display: bool) -> Result<String, MathError> {
|
||||
let formula = Formula {
|
||||
source: source.to_owned().into(),
|
||||
display,
|
||||
rgba: 0x0000_00ff,
|
||||
font: 1600,
|
||||
scale: 100,
|
||||
};
|
||||
svg(&formula).map(|(svg, _)| svg)
|
||||
}
|
||||
|
||||
/// The formula as outlined SVG holding all ink; its size.
|
||||
fn svg(formula: &Formula) -> Result<(String, Size<Pixels>), MathError> {
|
||||
let ast = parsed(&formula.source)?;
|
||||
|
||||
@@ -177,3 +177,11 @@ fn a_shared_formula_stays_until_its_last_holder_leaves(cx: &mut gpui::TestAppCon
|
||||
flush(cx);
|
||||
assert!(!cached("c^2", cx), "the last holder freed it");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_exported_formula_is_black_outlines() {
|
||||
let svg = formula_svg(r"\sqrt{x}", true).expect("typesets");
|
||||
assert!(svg.starts_with("<svg") && !svg.contains("<text"), "{svg}");
|
||||
assert!(svg.contains("0,0,0"), "black: {svg}");
|
||||
assert!(formula_svg(r"\frac{", false).is_err());
|
||||
}
|
||||
@@ -26,7 +26,7 @@ pub use formatted::{
|
||||
pub use inline::{Blockquote, Code, Highlight, Kbd, KbdCombo};
|
||||
pub use link::{ExternalLink, Link};
|
||||
pub(crate) use math::failed;
|
||||
pub use math::{Latex, MathError};
|
||||
pub use math::{Latex, MathError, formula_svg};
|
||||
pub use rolling::AnimatedNumber;
|
||||
pub use select::SelectableText;
|
||||
pub use text::{Caption, Heading, Label, Overline, Paragraph, Subtitle, Title, literal, tabular};
|
||||
Reference in new issue
Block a user