#[cfg(feature = "svg-fonts")]
use crate::commands::SvgGlyphLayer;
use crate::commands::{
Command, FillRule, FontOptions, FontVariant, FontVariationSetting, GlyphBounds,
GlyphGradientSpread, GlyphGradientUnits, GlyphLayer, GlyphLinearGradient, GlyphPaint,
GlyphRadialGradient, GlyphRun, PathPaintMode, PositionedGlyph, RasterGlyphLayer,
RasterGlyphSource, TextDirection,
};
use crate::fontface::FontFace;
use crate::util;
use base64::{engine::general_purpose, Engine as _};
use std::fmt::Write as _;
use std::io::{Error, ErrorKind};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ShapingPolicy {
LeftToRight,
RightToLeft,
TopToBottom,
}
impl Default for ShapingPolicy {
fn default() -> Self {
Self::LeftToRight
}
}
impl ShapingPolicy {
pub fn text_direction(self) -> TextDirection {
match self {
ShapingPolicy::LeftToRight => TextDirection::LeftToRight,
ShapingPolicy::RightToLeft => TextDirection::RightToLeft,
ShapingPolicy::TopToBottom => TextDirection::TopToBottom,
}
}
fn from_text_direction(text_direction: TextDirection) -> Self {
match text_direction {
TextDirection::LeftToRight => ShapingPolicy::LeftToRight,
TextDirection::RightToLeft => ShapingPolicy::RightToLeft,
TextDirection::TopToBottom => ShapingPolicy::TopToBottom,
}
}
}
#[derive(Clone)]
pub struct FontEngine<'a> {
face: &'a FontFace,
options: FontOptions<'a>,
shaping_policy: ShapingPolicy,
svg_unit: String,
}
impl<'a> FontEngine<'a> {
pub fn new(face: &'a FontFace) -> Self {
Self {
face,
options: FontOptions::new(face),
shaping_policy: ShapingPolicy::default(),
svg_unit: "px".to_string(),
}
}
pub fn with_options(mut self, options: FontOptions<'a>) -> Self {
self.shaping_policy = ShapingPolicy::from_text_direction(options.text_direction);
self.options = options.with_face(self.face);
self
}
pub fn with_font_size(mut self, font_size: f32) -> Self {
self.options = self.options.with_font_size(font_size);
self
}
pub fn with_line_height(mut self, line_height: f32) -> Self {
self.options = self.options.with_line_height(line_height);
self
}
pub fn with_locale(mut self, locale: &'a str) -> Self {
self.options = self.options.with_locale(locale);
self
}
pub fn with_font_variant(mut self, font_variant: FontVariant) -> Self {
self.options = self.options.with_font_variant(font_variant);
self
}
pub fn with_variation(mut self, tag: &str, value: f32) -> Self {
self.options = self.options.with_variation(tag, value);
self
}
pub fn with_variations(mut self, variations: &[FontVariationSetting]) -> Self {
self.options = self.options.with_variations(variations);
self
}
pub fn clear_variations(mut self) -> Self {
self.options = self.options.clear_variations();
self
}
pub fn with_jis78(self) -> Self {
self.with_font_variant(FontVariant::Jis78)
}
pub fn with_jis90(self) -> Self {
self.with_font_variant(FontVariant::Jis90)
}
pub fn with_traditional_forms(self) -> Self {
self.with_font_variant(FontVariant::TraditionalForms)
}
pub fn with_nlc_kanji_forms(self) -> Self {
self.with_font_variant(FontVariant::NlcKanjiForms)
}
pub fn with_shaping_policy(mut self, shaping_policy: ShapingPolicy) -> Self {
self.shaping_policy = shaping_policy;
self.options = self
.options
.with_text_direction(self.shaping_policy.text_direction());
self
}
pub fn with_left_to_right(self) -> Self {
self.with_shaping_policy(ShapingPolicy::LeftToRight)
}
pub fn with_right_to_left(self) -> Self {
self.with_shaping_policy(ShapingPolicy::RightToLeft)
}
pub fn with_vertical_flow(self) -> Self {
self.with_shaping_policy(ShapingPolicy::TopToBottom)
}
pub fn with_svg_unit(mut self, unit: impl Into<String>) -> Self {
self.svg_unit = unit.into();
self
}
pub fn shaping_policy(&self) -> ShapingPolicy {
self.shaping_policy
}
pub fn font_variant(&self) -> FontVariant {
self.options.font_variant
}
pub fn variation_settings(&self) -> &[FontVariationSetting] {
&self.options.variations
}
pub fn options(&self) -> FontOptions<'a> {
self.options
.clone()
.with_text_direction(self.shaping_policy.text_direction())
}
pub fn shape(&self, text: &str) -> Result<GlyphRun, Error> {
self.text2glyph_run(text)
}
pub fn text2glyph_run(&self, text: &str) -> Result<GlyphRun, Error> {
let mut options = self.options();
options.font = Some(crate::FontRef::Loaded(self.face));
crate::commands::text2commands(text, options)
}
pub fn text2commands(&self, text: &str) -> Result<GlyphRun, Error> {
self.text2glyph_run(text)
}
pub fn measure(&self, text: &str) -> Result<f64, Error> {
let mut options = self.options();
options.font = Some(crate::FontRef::Loaded(self.face));
self.face.font().measure_with_options(text, &options)
}
pub fn render_svg(&self, text: &str) -> Result<String, Error> {
self.text2svg(text)
}
pub fn render_svg_vertical(&self, text: &str) -> Result<String, Error> {
self.clone().with_vertical_flow().render_svg(text)
}
pub fn text2svg(&self, text: &str) -> Result<String, Error> {
let run = self.text2glyph_run(text)?;
glyph_run_to_svg(&run, &self.svg_unit)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "raw")]
fn japanese_font_path() -> std::path::PathBuf {
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join(".test_fonts")
.join("NotoSansJP-Regular.otf")
}
#[test]
fn shaping_policy_maps_to_text_direction() {
assert_eq!(
ShapingPolicy::LeftToRight.text_direction(),
TextDirection::LeftToRight
);
assert_eq!(
ShapingPolicy::RightToLeft.text_direction(),
TextDirection::RightToLeft
);
assert_eq!(
ShapingPolicy::TopToBottom.text_direction(),
TextDirection::TopToBottom
);
}
#[test]
#[cfg(feature = "raw")]
fn engine_stores_font_variant() {
let face = crate::FontFile::from_file(japanese_font_path())
.expect("load font file")
.current_face()
.expect("current face");
let engine = FontEngine::new(&face).with_jis78().with_vertical_flow();
assert_eq!(engine.font_variant(), FontVariant::Jis78);
assert_eq!(engine.shaping_policy(), ShapingPolicy::TopToBottom);
assert_eq!(engine.options().font_variant, FontVariant::Jis78);
assert_eq!(engine.options().text_direction, TextDirection::TopToBottom);
}
#[test]
fn glyph_run_to_svg_writes_stroke_path_attributes() {
let run = GlyphRun::new(vec![PositionedGlyph::new(
crate::Glyph::new(vec![GlyphLayer::Path(crate::PathGlyphLayer::stroke(
vec![Command::MoveTo(1.0, 2.0), Command::Line(3.0, 4.0)],
GlyphPaint::Solid(0xff11_2233),
2.5,
))]),
0.0,
0.0,
)]);
let svg = glyph_run_to_svg(&run, "px").expect("svg export");
assert!(svg.contains("fill=\"none\""));
assert!(svg.contains("stroke=\"#112233\""));
assert!(svg.contains("stroke-width=\"2.5\""));
}
#[test]
fn glyph_run_to_svg_writes_gradient_defs() {
let run = GlyphRun::new(vec![PositionedGlyph::new(
crate::Glyph::new(vec![GlyphLayer::Path(crate::PathGlyphLayer::new(
vec![Command::MoveTo(0.0, 0.0), Command::Line(10.0, 0.0), Command::Close],
GlyphPaint::LinearGradient(crate::GlyphLinearGradient {
x1: 0.0,
y1: 0.0,
x2: 10.0,
y2: 0.0,
units: crate::GlyphGradientUnits::UserSpaceOnUse,
transform: [1.0, 0.0, 0.0, 1.0, 0.0, 0.0],
spread: crate::GlyphGradientSpread::Pad,
stops: vec![
crate::GlyphGradientStop {
offset: 0.0,
color: 0xff11_2233,
},
crate::GlyphGradientStop {
offset: 1.0,
color: 0x8011_2233,
},
],
}),
))]),
0.0,
0.0,
)]);
let svg = glyph_run_to_svg(&run, "px").expect("svg export");
assert!(svg.contains("<defs><linearGradient"));
assert!(svg.contains("fill=\"url(#glyph-gradient-0)\""));
assert!(svg.contains("gradientUnits=\"userSpaceOnUse\""));
assert!(svg.contains("gradientTransform=\"matrix(1 0 0 1 0 0)\""));
assert!(svg.contains("stop-opacity=\""));
}
#[test]
fn glyph_run_to_svg_writes_clip_path_defs() {
let mut layer = crate::PathGlyphLayer::new(
vec![Command::MoveTo(0.0, 0.0), Command::Line(10.0, 0.0), Command::Close],
GlyphPaint::Solid(0xff11_2233),
);
layer.clip_commands = vec![
Command::MoveTo(1.0, 2.0),
Command::Line(3.0, 2.0),
Command::Line(3.0, 4.0),
Command::Close,
];
let run = GlyphRun::new(vec![PositionedGlyph::new(
crate::Glyph::new(vec![GlyphLayer::Path(layer)]),
0.0,
0.0,
)]);
let svg = glyph_run_to_svg(&run, "px").expect("svg export");
assert!(svg.contains("<clipPath id=\"glyph-clip-"));
assert!(svg.contains("clip-path=\"url(#glyph-clip-"));
}
}
pub(crate) fn glyph_run_to_svg(run: &GlyphRun, fontunit: &str) -> Result<String, Error> {
let Some(bounds) = glyph_run_bounds(run)? else {
let size = format!("0{}", fontunit);
return Ok(format!(
"<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"{}\" height=\"{}\" viewBox=\"0 0 0 0\"></svg>",
size, size
));
};
const SVG_EXPORT_PADDING: f32 = 4.0;
let min_x = bounds.min_x - SVG_EXPORT_PADDING;
let min_y = bounds.min_y - SVG_EXPORT_PADDING;
let view_width = (bounds.max_x - bounds.min_x + SVG_EXPORT_PADDING * 2.0).max(1.0);
let view_height = (bounds.max_y - bounds.min_y + SVG_EXPORT_PADDING * 2.0).max(1.0);
let width = view_width.ceil();
let height = view_height.ceil();
let mut svg = format!(
"<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"{}{}\" height=\"{}{}\" viewBox=\"{} {} {} {}\" overflow=\"visible\">",
width, fontunit, height, fontunit, min_x, min_y, view_width, view_height
);
let mut registry = SvgDefRegistry::default();
let mut body = String::new();
for glyph in &run.glyphs {
for layer in &glyph.glyph.layers {
match layer {
GlyphLayer::Path(path) => {
let d = draw_commands_to_svg_path(
&path.commands,
glyph.x + path.offset_x,
glyph.y + path.offset_y,
);
if d.is_empty() {
continue;
}
body += &format!(
"<path d=\"{}\" {} />",
d,
path_to_svg_attributes(path, &mut registry)
);
}
GlyphLayer::Raster(raster) => {
body += &raster_layer_to_svg_image(raster, glyph.x, glyph.y)?;
}
#[cfg(feature = "svg-fonts")]
GlyphLayer::Svg(layer) => {
body += &svg_layer_to_svg_fragment(layer, glyph.x, glyph.y);
}
}
}
}
if !registry.defs.is_empty() {
svg += "<defs>";
for definition in ®istry.defs {
svg += definition;
}
svg += "</defs>";
}
svg += &body;
svg += "</svg>";
Ok(svg)
}
fn glyph_run_bounds(run: &GlyphRun) -> Result<Option<GlyphBounds>, Error> {
let mut bounds = None;
for glyph in &run.glyphs {
let glyph_bounds = if let Some(bounds) = glyph.glyph.metrics.bounds {
Some(GlyphBounds {
min_x: bounds.min_x + glyph.x,
min_y: bounds.min_y + glyph.y,
max_x: bounds.max_x + glyph.x,
max_y: bounds.max_y + glyph.y,
})
} else {
glyph_layer_bounds(glyph)?
};
if let Some(glyph_bounds) = glyph_bounds {
extend_glyph_bounds(&mut bounds, glyph_bounds);
}
}
Ok(bounds)
}
fn glyph_layer_bounds(glyph: &PositionedGlyph) -> Result<Option<GlyphBounds>, Error> {
let mut bounds = None;
for layer in &glyph.glyph.layers {
match layer {
GlyphLayer::Path(path) => {
let stroke_padding = if matches!(path.paint_mode, PathPaintMode::Stroke) {
path.stroke_width / 2.0
} else {
0.0
};
for command in &path.commands {
match command {
Command::MoveTo(x, y) | Command::Line(x, y) => {
extend_path_point(
&mut bounds,
glyph.x + path.offset_x + *x,
glyph.y + path.offset_y + *y,
stroke_padding,
);
}
Command::Bezier((cx, cy), (x, y)) => {
extend_path_point(
&mut bounds,
glyph.x + path.offset_x + *cx,
glyph.y + path.offset_y + *cy,
stroke_padding,
);
extend_path_point(
&mut bounds,
glyph.x + path.offset_x + *x,
glyph.y + path.offset_y + *y,
stroke_padding,
);
}
Command::CubicBezier((xa, ya), (xb, yb), (xc, yc)) => {
extend_path_point(
&mut bounds,
glyph.x + path.offset_x + *xa,
glyph.y + path.offset_y + *ya,
stroke_padding,
);
extend_path_point(
&mut bounds,
glyph.x + path.offset_x + *xb,
glyph.y + path.offset_y + *yb,
stroke_padding,
);
extend_path_point(
&mut bounds,
glyph.x + path.offset_x + *xc,
glyph.y + path.offset_y + *yc,
stroke_padding,
);
}
Command::Close => {}
}
}
}
GlyphLayer::Raster(raster) => {
if let Some((width, height)) = raster_layer_dimensions(raster)? {
extend_point(
&mut bounds,
glyph.x + raster.offset_x,
glyph.y + raster.offset_y,
);
extend_point(
&mut bounds,
glyph.x + raster.offset_x + width,
glyph.y + raster.offset_y + height,
);
}
}
#[cfg(feature = "svg-fonts")]
GlyphLayer::Svg(layer) => {
extend_point(
&mut bounds,
glyph.x + layer.offset_x + layer.view_box_min_x,
glyph.y + layer.offset_y + layer.view_box_min_y,
);
extend_point(
&mut bounds,
glyph.x + layer.offset_x + layer.view_box_min_x + layer.view_box_width,
glyph.y + layer.offset_y + layer.view_box_min_y + layer.view_box_height,
);
}
}
}
Ok(bounds)
}
fn extend_glyph_bounds(bounds: &mut Option<GlyphBounds>, next: GlyphBounds) {
if let Some(bounds) = bounds.as_mut() {
bounds.min_x = bounds.min_x.min(next.min_x);
bounds.min_y = bounds.min_y.min(next.min_y);
bounds.max_x = bounds.max_x.max(next.max_x);
bounds.max_y = bounds.max_y.max(next.max_y);
} else {
*bounds = Some(next);
}
}
fn extend_point(bounds: &mut Option<GlyphBounds>, x: f32, y: f32) {
if let Some(bounds) = bounds.as_mut() {
bounds.min_x = bounds.min_x.min(x);
bounds.min_y = bounds.min_y.min(y);
bounds.max_x = bounds.max_x.max(x);
bounds.max_y = bounds.max_y.max(y);
} else {
*bounds = Some(GlyphBounds {
min_x: x,
min_y: y,
max_x: x,
max_y: y,
});
}
}
fn extend_path_point(bounds: &mut Option<GlyphBounds>, x: f32, y: f32, stroke_padding: f32) {
extend_point(bounds, x - stroke_padding, y - stroke_padding);
if stroke_padding > 0.0 {
extend_point(bounds, x + stroke_padding, y + stroke_padding);
}
}
fn draw_commands_to_svg_path(commands: &[Command], origin_x: f32, origin_y: f32) -> String {
let mut d = String::new();
for command in commands {
match command {
Command::MoveTo(x, y) => d += &format!("M{} {} ", origin_x + *x, origin_y + *y),
Command::Line(x, y) => d += &format!("L{} {} ", origin_x + *x, origin_y + *y),
Command::Bezier((cx, cy), (x, y)) => {
d += &format!(
"Q{} {} {} {} ",
origin_x + *cx,
origin_y + *cy,
origin_x + *x,
origin_y + *y
)
}
Command::CubicBezier((xa, ya), (xb, yb), (xc, yc)) => {
d += &format!(
"C{} {} {} {} {} {} ",
origin_x + *xa,
origin_y + *ya,
origin_x + *xb,
origin_y + *yb,
origin_x + *xc,
origin_y + *yc
)
}
Command::Close => d += "Z ",
}
}
d.trim_end().to_string()
}
fn normalize_svg_color(color: u32) -> u32 {
if color <= 0x00ff_ffff {
0xff00_0000 | color
} else {
color
}
}
fn paint_to_svg_attributes(paint: &GlyphPaint, registry: &mut SvgDefRegistry, attribute: &str) -> String {
match paint {
GlyphPaint::CurrentColor => format!("{attribute}=\"currentColor\""),
GlyphPaint::Solid(color) => {
let color = normalize_svg_color(*color);
let alpha = ((color >> 24) & 0xff) as u8;
let red = ((color >> 16) & 0xff) as u8;
let green = ((color >> 8) & 0xff) as u8;
let blue = (color & 0xff) as u8;
if alpha == 0xff {
format!("{attribute}=\"#{:02x}{:02x}{:02x}\"", red, green, blue)
} else {
format!(
"{attribute}=\"#{:02x}{:02x}{:02x}\" {attribute}-opacity=\"{}\"",
red,
green,
blue,
alpha as f32 / 255.0
)
}
}
GlyphPaint::LinearGradient(gradient) => {
let id = registry.register_linear_gradient(gradient);
format!("{attribute}=\"url(#{id})\"")
}
GlyphPaint::RadialGradient(gradient) => {
let id = registry.register_radial_gradient(gradient);
format!("{attribute}=\"url(#{id})\"")
}
}
}
fn path_to_svg_attributes(
path: &crate::commands::PathGlyphLayer,
registry: &mut SvgDefRegistry,
) -> String {
let clip = if path.clip_commands.is_empty() {
String::new()
} else {
let id = registry.register_clip_path(&path.clip_commands);
format!(" clip-path=\"url(#{id})\"")
};
match path.paint_mode {
PathPaintMode::Fill => format!(
"{}{}{}",
paint_to_svg_attributes(&path.paint, registry, "fill"),
fill_rule_to_svg_attribute(path.fill_rule),
clip
),
PathPaintMode::Stroke => format!(
"fill=\"none\" {} stroke-width=\"{}\"{}",
paint_to_svg_attributes(&path.paint, registry, "stroke"),
path.stroke_width,
clip
),
}
}
#[derive(Default)]
struct SvgDefRegistry {
defs: Vec<String>,
next_id: usize,
}
impl SvgDefRegistry {
fn register_linear_gradient(&mut self, gradient: &GlyphLinearGradient) -> String {
let id = format!("glyph-gradient-{}", self.next_id);
self.next_id += 1;
self.defs.push(linear_gradient_to_svg_def(&id, gradient));
id
}
fn register_radial_gradient(&mut self, gradient: &GlyphRadialGradient) -> String {
let id = format!("glyph-gradient-{}", self.next_id);
self.next_id += 1;
self.defs.push(radial_gradient_to_svg_def(&id, gradient));
id
}
fn register_clip_path(&mut self, commands: &[Command]) -> String {
let id = format!("glyph-clip-{}", self.next_id);
self.next_id += 1;
self.defs.push(clip_path_to_svg_def(&id, commands));
id
}
}
fn linear_gradient_to_svg_def(id: &str, gradient: &GlyphLinearGradient) -> String {
let mut definition = format!(
"<linearGradient id=\"{}\" x1=\"{}\" y1=\"{}\" x2=\"{}\" y2=\"{}\" gradientUnits=\"{}\" gradientTransform=\"{}\" spreadMethod=\"{}\">",
id,
gradient.x1,
gradient.y1,
gradient.x2,
gradient.y2,
gradient_units_to_svg(gradient.units),
gradient_transform_to_svg(&gradient.transform),
gradient_spread_to_svg(gradient.spread)
);
append_gradient_stops(&mut definition, &gradient.stops);
definition += "</linearGradient>";
definition
}
fn radial_gradient_to_svg_def(id: &str, gradient: &GlyphRadialGradient) -> String {
let mut definition = format!(
"<radialGradient id=\"{}\" cx=\"{}\" cy=\"{}\" r=\"{}\" fx=\"{}\" fy=\"{}\" gradientUnits=\"{}\" gradientTransform=\"{}\" spreadMethod=\"{}\">",
id,
gradient.cx,
gradient.cy,
gradient.r,
gradient.fx,
gradient.fy,
gradient_units_to_svg(gradient.units),
gradient_transform_to_svg(&gradient.transform),
gradient_spread_to_svg(gradient.spread)
);
append_gradient_stops(&mut definition, &gradient.stops);
definition += "</radialGradient>";
definition
}
fn clip_path_to_svg_def(id: &str, commands: &[Command]) -> String {
format!(
"<clipPath id=\"{}\"><path d=\"{}\"/></clipPath>",
id,
draw_commands_to_svg_path(commands, 0.0, 0.0)
)
}
fn append_gradient_stops(definition: &mut String, stops: &[crate::GlyphGradientStop]) {
for stop in stops {
let color = normalize_svg_color(stop.color);
let alpha = ((color >> 24) & 0xff) as u8;
let red = ((color >> 16) & 0xff) as u8;
let green = ((color >> 8) & 0xff) as u8;
let blue = (color & 0xff) as u8;
let _ = if alpha == 0xff {
write!(
definition,
"<stop offset=\"{}%\" stop-color=\"#{:02x}{:02x}{:02x}\"/>",
stop.offset * 100.0,
red,
green,
blue
)
} else {
write!(
definition,
"<stop offset=\"{}%\" stop-color=\"#{:02x}{:02x}{:02x}\" stop-opacity=\"{}\"/>",
stop.offset * 100.0,
red,
green,
blue,
alpha as f32 / 255.0
)
};
}
}
fn gradient_spread_to_svg(spread: GlyphGradientSpread) -> &'static str {
match spread {
GlyphGradientSpread::Pad => "pad",
GlyphGradientSpread::Reflect => "reflect",
GlyphGradientSpread::Repeat => "repeat",
}
}
fn gradient_units_to_svg(units: GlyphGradientUnits) -> &'static str {
match units {
GlyphGradientUnits::ObjectBoundingBox => "objectBoundingBox",
GlyphGradientUnits::UserSpaceOnUse => "userSpaceOnUse",
}
}
fn gradient_transform_to_svg(transform: &[f32; 6]) -> String {
format!(
"matrix({} {} {} {} {} {})",
transform[0], transform[1], transform[2], transform[3], transform[4], transform[5]
)
}
fn fill_rule_to_svg_attribute(fill_rule: FillRule) -> &'static str {
match fill_rule {
FillRule::NonZero => "",
FillRule::EvenOdd => " fill-rule=\"evenodd\"",
}
}
fn raster_layer_to_svg_image(
raster: &RasterGlyphLayer,
glyph_x: f32,
glyph_y: f32,
) -> Result<String, Error> {
let Some((width, height)) = raster_layer_dimensions(raster)? else {
return Ok(String::new());
};
match &raster.source {
RasterGlyphSource::Encoded(data) => {
let Some((mime, _, _)) = util::sniff_encoded_image_dimensions(data) else {
return Err(Error::new(
ErrorKind::Unsupported,
"encoded raster glyph format is not supported for SVG export",
));
};
let encoded = general_purpose::STANDARD.encode(data);
Ok(format!(
"<image x=\"{}\" y=\"{}\" width=\"{}\" height=\"{}\" href=\"data:{};base64,{}\"/>",
glyph_x + raster.offset_x,
glyph_y + raster.offset_y,
width,
height,
mime,
encoded
))
}
RasterGlyphSource::Rgba { .. } => Err(Error::new(
ErrorKind::Unsupported,
"RGBA raster glyph layers are not supported for SVG export yet",
)),
}
}
fn raster_layer_dimensions(raster: &RasterGlyphLayer) -> Result<Option<(f32, f32)>, Error> {
let (source_width, source_height) = match &raster.source {
RasterGlyphSource::Encoded(data) => {
let Some((_, width, height)) = util::sniff_encoded_image_dimensions(data) else {
return Err(Error::new(
ErrorKind::Unsupported,
"encoded raster glyph format is not supported for SVG export",
));
};
(width, height)
}
RasterGlyphSource::Rgba { width, height, .. } => (*width, *height),
};
let width = raster.width.unwrap_or(source_width);
let height = raster.height.unwrap_or(source_height);
if width == 0 || height == 0 {
return Ok(None);
}
Ok(Some((width as f32, height as f32)))
}
#[cfg(feature = "svg-fonts")]
fn svg_layer_to_svg_fragment(layer: &SvgGlyphLayer, glyph_x: f32, glyph_y: f32) -> String {
format!(
"<svg x=\"{}\" y=\"{}\" width=\"{}\" height=\"{}\" viewBox=\"{} {} {} {}\" overflow=\"visible\">{}</svg>",
glyph_x + layer.offset_x,
glyph_y + layer.offset_y,
layer.width,
layer.height,
layer.view_box_min_x,
layer.view_box_min_y,
layer.view_box_width,
layer.view_box_height,
layer.document
)
}