use std::sync::Arc;
use resvg::{tiny_skia, usvg};
use crate::error::{ChartError, ChartWarning};
const FONTS: [&[u8]; 4] = [
include_bytes!("../fonts/Inter-Regular-latin.ttf"),
include_bytes!("../fonts/Inter-SemiBold-latin.ttf"),
include_bytes!("../fonts/Inter-Regular-latin-ext.ttf"),
include_bytes!("../fonts/Inter-SemiBold-latin-ext.ttf"),
];
pub(crate) const MAX_SCALE: f32 = 4.0;
pub(crate) fn rasterize(
svg: &str,
scale: f32,
brand: Option<(&str, &[u8])>,
) -> Result<Vec<u8>, ChartError> {
let mut fonts = usvg::fontdb::Database::new();
for font in FONTS {
fonts.load_font_data(font.to_vec());
}
let semibold: Vec<usvg::fontdb::FaceInfo> = fonts
.faces()
.filter(|face| {
face.families
.iter()
.any(|(name, _)| name == "Inter SemiBold")
})
.cloned()
.collect();
for mut face in semibold {
fonts.remove_face(face.id);
face.families = vec![(
"Inter".to_owned(),
usvg::fontdb::Language::English_UnitedStates,
)];
fonts.push_face_info(face);
}
fonts.set_sans_serif_family("Inter");
if let Some((family, data)) = brand {
let known: Vec<usvg::fontdb::ID> = fonts.faces().map(|face| face.id).collect();
fonts.load_font_data(data.to_vec());
let added: Vec<usvg::fontdb::FaceInfo> = fonts
.faces()
.filter(|face| !known.contains(&face.id))
.cloned()
.collect();
for mut face in added {
fonts.remove_face(face.id);
face.families = vec![(
family.to_owned(),
usvg::fontdb::Language::English_UnitedStates,
)];
fonts.push_face_info(face);
}
}
let options = usvg::Options {
font_family: "Inter".to_owned(),
fontdb: Arc::new(fonts),
..usvg::Options::default()
};
let tree = usvg::Tree::from_str(svg, &options).map_err(|error| {
ChartError::new(
"png_failed",
"/render/format",
format!("could not read the chart SVG: {error}"),
)
})?;
let size = tree
.size()
.to_int_size()
.scale_by(scale)
.expect("a validated scale keeps the chart larger than a pixel");
let mut pixmap = tiny_skia::Pixmap::new(size.width(), size.height())
.expect("a validated scale keeps the image within memory limits");
resvg::render(
&tree,
tiny_skia::Transform::from_scale(scale, scale),
&mut pixmap.as_mut(),
);
pixmap.encode_png().map_err(|error| {
ChartError::new(
"png_failed",
"/render/format",
format!("could not encode the PNG: {error}"),
)
})
}
fn covered() -> &'static [u32] {
static COVERED: std::sync::OnceLock<Vec<u32>> = std::sync::OnceLock::new();
COVERED.get_or_init(|| {
let mut codes: Vec<u32> = [FONTS[0], FONTS[2]]
.into_iter()
.flat_map(cmap_codes)
.collect();
codes.sort_unstable();
codes.dedup();
codes
})
}
fn read_u16(data: &[u8], at: usize) -> u16 {
u16::from_be_bytes([data[at], data[at + 1]])
}
fn read_u32(data: &[u8], at: usize) -> u32 {
u32::from_be_bytes([data[at], data[at + 1], data[at + 2], data[at + 3]])
}
fn cmap_codes(font: &[u8]) -> Vec<u32> {
let tables = usize::from(read_u16(font, 4));
let cmap = (0..tables)
.map(|index| 12 + 16 * index)
.find(|record| &font[*record..*record + 4] == b"cmap")
.map(|record| read_u32(font, record + 8) as usize)
.expect("the bundled fonts have a character map");
let subtables = usize::from(read_u16(font, cmap + 2));
let table = (0..subtables)
.map(|index| cmap + 4 + 8 * index)
.find(|record| read_u16(font, *record) == 3 && read_u16(font, *record + 2) == 1)
.map(|record| cmap + read_u32(font, record + 4) as usize)
.expect("the bundled fonts have a Windows Unicode character map");
let segments = usize::from(read_u16(font, table + 6)) / 2;
let ends = table + 14;
let starts = ends + 2 * segments + 2;
let deltas = starts + 2 * segments;
let offsets = deltas + 2 * segments;
let mut codes = Vec::new();
for segment in 0..segments {
let end = read_u16(font, ends + 2 * segment);
let start = read_u16(font, starts + 2 * segment);
let delta = read_u16(font, deltas + 2 * segment);
let offset_at = offsets + 2 * segment;
let offset = usize::from(read_u16(font, offset_at));
for code in start..=end {
if code == 0xFFFF {
continue;
}
let glyph = if offset == 0 {
code.wrapping_add(delta)
} else {
let at = offset_at + offset + 2 * usize::from(code - start);
match read_u16(font, at) {
0 => 0,
glyph => glyph.wrapping_add(delta),
}
};
if glyph != 0 {
codes.push(u32::from(code));
}
}
}
codes
}
pub(crate) fn missing_glyphs(svg: &str) -> Option<ChartWarning> {
let mut missing: Vec<char> = Vec::new();
for text in svg.split("<text").skip(1) {
let content = text
.split_once('>')
.and_then(|(_, rest)| rest.split_once("</text>"))
.map_or("", |(content, _)| content);
for character in content.chars() {
let covered =
character.is_whitespace() || covered().binary_search(&u32::from(character)).is_ok();
if !covered && !missing.contains(&character) {
missing.push(character);
}
}
}
(!missing.is_empty()).then(|| {
ChartWarning::new(
"glyph_missing",
"/render/format",
format!(
"the PNG font has no glyph for {}; these characters are drawn as boxes, while the SVG and HTML output show them",
missing.iter().collect::<String>()
),
)
})
}