Skip to content

Commit

Permalink
feat: add svg render methods to Font
Browse files Browse the repository at this point in the history
  • Loading branch information
KonghaYao committed Nov 13, 2024
1 parent a8149ac commit b948254
Show file tree
Hide file tree
Showing 4 changed files with 148 additions and 30 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ categories = [
"text-processing",
]
license = "MIT"
edition = "2018"
edition = "2021"
links = "harfbuzz"
include = [
"src/*",
Expand Down
33 changes: 4 additions & 29 deletions src/font.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ use std::ptr::NonNull;
use std::os::raw::c_void;

use crate::bindings::{
hb_codepoint_t, hb_font_create, hb_font_create_sub_font, hb_font_destroy, hb_font_draw_glyph,
hb_font_create, hb_font_create_sub_font, hb_font_destroy, hb_font_draw_glyph,
hb_font_extents_t, hb_font_get_empty, hb_font_get_face, hb_font_get_glyph_contour_point,
hb_font_get_glyph_extents, hb_font_get_glyph_from_name, hb_font_get_glyph_h_advance,
hb_font_get_glyph_h_origin, hb_font_get_glyph_name, hb_font_get_glyph_v_advance,
hb_font_get_glyph_v_origin, hb_font_get_h_extents, hb_font_get_nominal_glyph,
hb_font_get_parent, hb_font_get_ppem, hb_font_get_scale, hb_font_get_v_extents,
hb_font_get_variation_glyph, hb_font_reference, hb_font_set_funcs, hb_font_set_ppem,
hb_font_set_scale, hb_font_set_variations, hb_font_t, hb_glyph_extents_t, hb_glyph_to_svg_path,
hb_font_set_scale, hb_font_set_variations, hb_font_t, hb_glyph_extents_t,
hb_position_t,
};
use crate::common::{HarfbuzzObject, Owned, Shared};
Expand All @@ -19,7 +19,7 @@ use crate::draw_funcs::DrawFuncsImpl;
use crate::face::Face;
pub use crate::font_funcs::FontFuncs;
use crate::font_funcs::FontFuncsImpl;
use crate::Variation;
use crate::{Variation};

use std::ffi::CStr;
use std::marker::PhantomData;
Expand Down Expand Up @@ -498,24 +498,9 @@ impl<'a> Font<'a> {
)
};
}

pub fn glyph_to_svg_path(&mut self, glyph: hb_codepoint_t) -> String {
const PATH_BUFFER_SIZE: u32 = 65536; // should be enough for most glyphs
let mut path_buffer = vec![0_i8; PATH_BUFFER_SIZE as usize];
unsafe {
hb_glyph_to_svg_path(
self.as_raw_mut(),
glyph,
path_buffer.as_mut_ptr(),
PATH_BUFFER_SIZE,
);
}
let end_pos = path_buffer.iter().position(|&x| x == 0).unwrap();
let u8_array: Vec<u8> = path_buffer[..end_pos].iter().map(|&x| x as u8).collect();
String::from_utf8(u8_array).unwrap()
}
}


unsafe impl<'a> Send for Font<'a> {}
unsafe impl<'a> Sync for Font<'a> {}

Expand Down Expand Up @@ -564,14 +549,4 @@ mod test {
assert_memory_layout_equal::<FontExtents, hb_font_extents_t>()
}

#[test]
fn test_glyph_to_svg() {
let path = "testfiles/SourceSansVariable-Roman.ttf";
let face = Face::from_file(path, 0).unwrap();
let mut font = Font::new(face);
let graph_str = font.glyph_to_svg_path(b'h' as u32);
assert_eq!(
graph_str,
"M100,0L100,660L454,660L454,632L132,632L132,366L402,366L402,338L132,338L132,28L464,28L464,0L100,0ZM185,710L171,726L267,808L295,808L391,726L377,710L283,778L279,778L185,710ZM143,844Q144,860 149.5,877.5Q155,895 169.5,907.5Q184,920 211,920Q237,920 255.5,912Q274,904 289,893Q304,882 318.5,874Q333,866 353,866Q374,866 383,880.5Q392,895 395,918L419,916Q418,900 412.5,882.5Q407,865 392.5,852.5Q378,840 351,840Q325,840 306.5,848Q288,856 273,867Q258,878 243.5,886Q229,894 209,894Q188,894 179,879.5Q170,865 167,842L143,844Z")
}
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ mod utils;
pub mod rusttype;

pub mod subset;
pub mod svg;

use bindings::hb_feature_t;
use bindings::hb_shape;
Expand Down
142 changes: 142 additions & 0 deletions src/svg.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
use crate::{
bindings::{hb_codepoint_t, hb_glyph_to_svg_path},
font::Font,
shape, Feature, GlyphPosition, HarfbuzzObject, UnicodeBuffer,
};
#[derive(Debug)]
pub struct GlyphInfoWithSvgPath {
svg_path: String,
position: GlyphPosition,
}

#[derive(Debug)]
pub struct BoundingBox {
height: f64,
width: f64,
}

impl<'a> Font<'a> {
pub fn glyph_to_svg_path(&mut self, glyph: hb_codepoint_t) -> String {
const PATH_BUFFER_SIZE: u32 = 65536; // should be enough for most glyphs
let mut path_buffer = vec![0_i8; PATH_BUFFER_SIZE as usize];
unsafe {
hb_glyph_to_svg_path(
self.as_raw(),
glyph,
path_buffer.as_mut_ptr(),
PATH_BUFFER_SIZE,
);
}
let end_pos = path_buffer.iter().position(|&x| x == 0).unwrap();
let u8_array: Vec<u8> = path_buffer[..end_pos].iter().map(|&x| x as u8).collect();
String::from_utf8(u8_array).unwrap()
}
fn sharp_text_to_glyphs(
&mut self,
text: &str,
features: &[Feature],
) -> Vec<GlyphInfoWithSvgPath> {
self.set_scale(100, -100);

let buffer = UnicodeBuffer::new()
.add_str(text)
.guess_segment_properties()
.set_direction(crate::Direction::Ltr);
let result_buffer = shape(self, buffer, features);
let infos = result_buffer.get_glyph_infos();
let positions = result_buffer.get_glyph_positions();
let result: Vec<GlyphInfoWithSvgPath> = positions
.iter()
.zip(infos)
.map(|(position, info)| GlyphInfoWithSvgPath {
svg_path: self.glyph_to_svg_path(info.codepoint),
position: *position,
})
.collect();
result
}
pub fn render_svg_text(&mut self, text: &str, features: &[Feature]) -> String {
let base_line = 24;
let line_height = 1.0;
let line_height_px = 100.0 * line_height;
let rows: Vec<Vec<GlyphInfoWithSvgPath>> = text
.split('\n')
.map(|t| self.sharp_text_to_glyphs(t, features))
.collect();

let mut max_bounding = BoundingBox {
height: 0.0,
width: 0.0,
};

let mut bounding = BoundingBox {
height: line_height_px,
width: 0.0,
};

let paths: Vec<String> = rows
.iter()
.flat_map(|row| {
let rendered_row: Vec<String> = row
.iter()
.map(|glyph| {
let path: String = format!(
r#"<path transform="translate({} {})" d="{}"></path>"#,
bounding.width,
bounding.height + glyph.position.y_advance as f64,
glyph.svg_path
);
bounding.width += glyph.position.x_advance as f64;
path
})
.collect();
// set the maximum bounding box
max_bounding.height = max_bounding.height.max(bounding.height as f64);
max_bounding.width = max_bounding.width.max(bounding.width as f64);

// reset row width
bounding.height += line_height_px;
bounding.width = 0.0;

rendered_row
})
.collect();

format!(
r#"<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="{}" height="{}" viewBox="0 0 {} {}">{}</svg>"#,
max_bounding.width,
max_bounding.height,
max_bounding.width + base_line as f64,
max_bounding.height + base_line as f64,
paths.join("")
)
}
}
#[cfg(test)]
mod test {

use crate::Face;

use super::*;

#[test]
fn test_glyph_to_svg() {
let path = "testfiles/SourceSansVariable-Roman.ttf";
let face = Face::from_file(path, 0).unwrap();
let mut font = Font::new(face);
let graph_str = font.glyph_to_svg_path(b'h' as u32);
assert_eq!(
graph_str,
"M100,0L100,660L454,660L454,632L132,632L132,366L402,366L402,338L132,338L132,28L464,28L464,0L100,0ZM185,710L171,726L267,808L295,808L391,726L377,710L283,778L279,778L185,710ZM143,844Q144,860 149.5,877.5Q155,895 169.5,907.5Q184,920 211,920Q237,920 255.5,912Q274,904 289,893Q304,882 318.5,874Q333,866 353,866Q374,866 383,880.5Q392,895 395,918L419,916Q418,900 412.5,882.5Q407,865 392.5,852.5Q378,840 351,840Q325,840 306.5,848Q288,856 273,867Q258,878 243.5,886Q229,894 209,894Q188,894 179,879.5Q170,865 167,842L143,844Z")
}
#[test]
fn test_svg_text() {
let path = "testfiles/SourceSansVariable-Roman.ttf";
let face = Face::from_file(path, 0).unwrap();
let mut font = Font::new(face);
let features = [Feature::new(b"liga", 1, 0..10000)];
let graph_str = font.render_svg_text("Hello World\nIt's me!\nliga feature fft!", &features);
assert_eq!(graph_str.starts_with("<svg"), true);
assert_eq!(graph_str.ends_with("</path></svg>"), true);
}
}

0 comments on commit b948254

Please sign in to comment.