mod icon2compose;
mod icon2svg;
mod icon2xml;
use crate::{
error::DrawSvgError, iconid::IconIdentifier, pathstyle::SvgPathStyle, pens::SvgPathPen,
};
use kurbo::Affine;
use skrifa::raw::TableProvider;
use skrifa::{
color::ColorGlyph,
instance::{LocationRef, Size},
metrics::{BoundingBox, GlyphMetrics},
outline::{pen::PathStyle, DrawSettings, OutlinePen},
FontRef, GlyphId, MetadataProvider, OutlineGlyph,
};
pub struct DrawOptions<'a> {
pub identifier: IconIdentifier,
pub height: f32,
pub location: LocationRef<'a>,
pub style: SvgPathStyle,
pub viewbox_mode: ViewBoxMode,
pub additional_attributes: Vec<String>,
pub fill_color: Option<u32>,
pub draw_type: DrawType<'a>,
}
#[derive(Debug)]
pub enum DrawType<'a> {
Svg,
AndroidVectorDrawable,
ComposeImageVector {
variable_name: &'a str,
package: &'a str,
},
}
pub enum ViewBoxMode {
Auto,
UseHeight,
UseBoundingBox,
}
impl<'a> DrawOptions<'a> {
pub fn new(
identifier: IconIdentifier,
height: f32,
location: LocationRef<'a>,
style: SvgPathStyle,
draw_type: DrawType<'a>,
) -> DrawOptions<'a> {
DrawOptions {
identifier,
height,
location,
style,
viewbox_mode: ViewBoxMode::Auto,
additional_attributes: Vec::new(),
fill_color: None,
draw_type,
}
}
fn viewbox_for_svg(
&self,
bounding_box: Option<BoundingBox>,
upem: u16,
advance_height: f64,
advance_width: f64,
) -> ViewBox {
match self.viewbox_mode {
ViewBoxMode::Auto => self.viewbox_auto(advance_height, advance_width),
ViewBoxMode::UseHeight => ViewBox {
x: 0.0,
y: 0.0,
width: (advance_width * (self.height / upem as f32) as f64).round(),
height: self.height as f64,
},
ViewBoxMode::UseBoundingBox => {
if let Some(bbox) = bounding_box {
let width = bbox.x_max - bbox.x_min;
let height = bbox.y_max - bbox.y_min;
ViewBox {
x: bbox.x_min as f64,
y: -bbox.y_max as f64,
width: width as f64,
height: height as f64,
}
} else {
self.viewbox_auto(advance_height, advance_width)
}
}
}
}
fn viewbox_auto(&self, advance_height: f64, advance_width: f64) -> ViewBox {
ViewBox {
x: 0.0,
y: -advance_height,
width: advance_width,
height: advance_height,
}
}
fn viewbox_for_android(
&self,
bounding_box: Option<BoundingBox>,
upem: u16,
advance_height: f64,
advance_width: f64,
) -> ViewBox {
ViewBox {
x: 0.0,
y: 0.0,
..self.viewbox_for_svg(bounding_box, upem, advance_height, advance_width)
}
}
fn prepare_drawing_instructions(
&self,
font: &FontRef<'a>,
) -> Result<DrawingInstructions<'a>, DrawSvgError> {
let gid = self
.identifier
.resolve(font, self.location)
.map_err(|e| DrawSvgError::ResolutionError(self.identifier.clone(), e))?;
let head = font
.head()
.map_err(|e| DrawSvgError::ReadError("head", e))?;
let upem = head.units_per_em();
let metrics = GlyphMetrics::new(font, Size::unscaled(), self.location);
let advance_width = metrics.advance_width(gid).unwrap_or(upem as f32);
let hhea = font
.hhea()
.map_err(|e| DrawSvgError::ReadError("hhea", e))?;
let advance_height = (hhea.ascender().to_i16() + hhea.descender().to_i16()) as f64;
let glyph = font
.color_glyphs()
.get(gid)
.map(GlyphType::Color)
.or_else(|| font.outline_glyphs().get(gid).map(GlyphType::Outline))
.ok_or(DrawSvgError::NoOutline(self.identifier.clone(), gid))?;
let bounding_box = match glyph {
GlyphType::Outline(ref _glyph) => metrics.bounds(gid),
GlyphType::Color(ref color_glyph) => {
color_glyph.bounding_box(self.location, Size::unscaled())
}
};
let viewbox = match self.draw_type {
DrawType::Svg => {
self.viewbox_for_svg(bounding_box, upem, advance_height, advance_width.into())
}
DrawType::AndroidVectorDrawable => {
self.viewbox_for_android(bounding_box, upem, advance_height, advance_width.into())
}
DrawType::ComposeImageVector { .. } => {
self.viewbox_for_android(bounding_box, upem, advance_height, advance_width.into())
}
};
let glyph_width = (self.height as f64 * (viewbox.width / viewbox.height)) as u16;
Ok(DrawingInstructions {
glyph,
viewbox,
upem,
glyph_id: gid,
glyph_width,
})
}
}
#[allow(clippy::large_enum_variant)]
pub(crate) enum GlyphType<'a> {
Outline(OutlineGlyph<'a>),
Color(ColorGlyph<'a>),
}
pub(crate) struct DrawingInstructions<'a> {
pub glyph: GlyphType<'a>,
pub glyph_id: GlyphId,
pub viewbox: ViewBox,
pub upem: u16,
pub glyph_width: u16,
}
pub trait DrawIcon {
fn draw_icon(&self, options: &DrawOptions) -> Result<String, DrawSvgError>;
}
impl DrawIcon for FontRef<'_> {
fn draw_icon(&self, options: &DrawOptions) -> Result<String, DrawSvgError> {
let di = options.prepare_drawing_instructions(self)?;
match options.draw_type {
DrawType::Svg => icon2svg::draw_svg(self, di, options),
DrawType::AndroidVectorDrawable => icon2xml::draw_android_vector_drawable(di, options),
DrawType::ComposeImageVector { .. } => {
icon2compose::draw_compose_image_vector(di, options)
}
}
}
}
fn get_pen(viewbox: ViewBox, upem: u16) -> SvgPathPen {
let scale = viewbox.height / upem as f64;
let translate_y = viewbox.height + viewbox.y;
SvgPathPen::new_with_transform(Affine::new([scale, 0.0, 0.0, -scale, 0.0, translate_y]))
}
#[derive(Copy, Clone, Debug)]
pub(crate) struct ViewBox {
pub x: f64,
pub y: f64,
pub width: f64,
pub height: f64,
}
fn draw_glyph(
glyph: OutlineGlyph<'_>,
options: &DrawOptions<'_>,
pen: &mut impl OutlinePen,
) -> Result<(), DrawSvgError> {
glyph
.draw(
DrawSettings::unhinted(Size::unscaled(), options.location)
.with_path_style(PathStyle::HarfBuzz),
pen,
)
.map_err(|e| DrawSvgError::DrawError(options.identifier.clone(), glyph.glyph_id(), e))?;
Ok(())
}