#![cfg(feature = "html")]
use printpdf::*;
use std::collections::BTreeMap;
struct SvgText {
font_family: String,
y_attr: f32,
matrix: [f32; 6],
content: String,
}
fn attr<'a>(tag: &'a str, name: &str) -> Option<&'a str> {
let key = format!("{}=\"", name);
let start = tag.find(&key)? + key.len();
let rest = &tag[start..];
let end = rest.find('"')?;
Some(&rest[..end])
}
fn parse_matrix(tag: &str) -> Option<[f32; 6]> {
let start = tag.find("transform=\"matrix(")? + "transform=\"matrix(".len();
let rest = &tag[start..];
let end = rest.find(')')?;
let nums: Vec<f32> = rest[..end]
.split_whitespace()
.filter_map(|s| s.parse::<f32>().ok())
.collect();
if nums.len() == 6 {
Some([nums[0], nums[1], nums[2], nums[3], nums[4], nums[5]])
} else {
None
}
}
fn parse_texts(svg: &str) -> Vec<SvgText> {
let mut out = Vec::new();
let mut cursor = 0;
while let Some(open_rel) = svg[cursor..].find("<text") {
let open = cursor + open_rel;
let tag_end = match svg[open..].find('>') {
Some(i) => open + i,
None => break,
};
let tag = &svg[open..tag_end];
let close = match svg[tag_end..].find("</text>") {
Some(i) => tag_end + i,
None => break,
};
let content = &svg[tag_end + 1..close];
if let (Some(fam), Some(y), Some(m)) = (
attr(tag, "font-family"),
attr(tag, "y").and_then(|v| v.parse::<f32>().ok()),
parse_matrix(tag),
) {
out.push(SvgText {
font_family: fam.to_string(),
y_attr: y,
matrix: m,
content: content.to_string(),
});
}
cursor = close + "</text>".len();
}
out
}
fn font_face_family(svg: &str) -> Option<String> {
let start = svg.find("@font-face")?;
let rest = &svg[start..];
let key = "font-family: \"";
let fstart = rest.find(key)? + key.len();
let frest = &rest[fstart..];
let end = frest.find('"')?;
Some(frest[..end].to_string())
}
fn font_face_has_base64(svg: &str) -> bool {
if let Some(i) = svg.find("base64,") {
let after = &svg[i + "base64,".len()..];
let len: usize = after.chars().take_while(|c| *c != '"').count();
len > 1000
} else {
false
}
}
#[test]
fn svg_preview_text_is_visible_upright_onpage_in_embedded_font() {
let font = std::fs::read("examples/assets/fonts/RobotoMedium.ttf").unwrap();
let mut fonts = BTreeMap::new();
fonts.insert("Roboto Medium".to_string(), Base64OrRaw::Raw(font));
let html = r#"<html><body><p style="font-family:'Roboto Medium';font-size:24px;">HELLO PREVIEW</p></body></html>"#;
let mut w = Vec::new();
let doc = PdfDocument::from_html(
html,
&BTreeMap::new(),
&fonts,
&GeneratePdfOptions::default(),
&mut w,
)
.unwrap();
let page = &doc.pages[0];
let page_height = page.media_box.height.0;
let svg = page.to_svg(&doc.resources, &PdfToSvgOptions::default(), &mut w);
let texts = parse_texts(&svg);
assert!(
!texts.is_empty(),
"preview SVG contains no parseable <text> elements"
);
let joined: String = texts.iter().map(|t| t.content.as_str()).collect();
assert!(
joined.contains("HELLO"),
"preview SVG text does not contain readable unicode 'HELLO' (got {joined:?})"
);
for t in &texts {
let effective_y = t.matrix[1] * 0.0 + t.matrix[3] * t.y_attr + t.matrix[5];
assert!(
effective_y >= 0.0 && effective_y <= page_height,
"glyph {:?} is off-page: effective y = {effective_y} not in [0, {page_height}] \
(y_attr={}, matrix={:?})",
t.content,
t.y_attr,
t.matrix,
);
assert!(
t.matrix[3] > 0.0,
"glyph {:?} is vertically mirrored (matrix d = {} <= 0)",
t.content,
t.matrix[3],
);
}
let text_family = &texts[0].font_family;
assert!(
!text_family.contains("Times") && !text_family.contains("serif"),
"text fell back to a builtin family instead of the embedded font: {text_family:?}"
);
let face_family =
font_face_family(&svg).expect("no @font-face rule emitted for the embedded custom font");
assert_eq!(
&face_family, text_family,
"@font-face family {face_family:?} does not match <text> font-family {text_family:?}; \
the browser would fall back to a default font"
);
assert!(
font_face_has_base64(&svg),
"@font-face does not embed base64 font bytes"
);
}