Skip to content

Commit

Permalink
feat(text): 🎸 svg emoji
Browse files Browse the repository at this point in the history
  • Loading branch information
wjian23 committed Sep 26, 2023
1 parent 07373e6 commit 17e6785
Show file tree
Hide file tree
Showing 6 changed files with 313 additions and 29 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ webbrowser = "0.8.8"
wgpu = {version = "0.16.0"}
winit = {version = "0.28.5", default-features = false, features = ["x11", "wayland", "wayland-dlopen"]}
zerocopy = "0.7.3"
quick-xml = "0.30.0"

[workspace.metadata.release]
shared-version = true
Expand Down
2 changes: 2 additions & 0 deletions text/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ rustybuzz.workspace = true
unicode-bidi.workspace = true
unicode-script.workspace = true
unicode-segmentation.workspace = true
quick-xml.workspace = true


[features]
default = ["raster_png_font"]
Expand Down
87 changes: 66 additions & 21 deletions text/src/font_db.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
use fontdb::{Database, Query};
pub use fontdb::{FaceInfo, Family, ID};
use lyon_path::math::Point;
use ribir_algo::ShareResource;
use ribir_painter::{PixelImage, Svg};
use rustybuzz::ttf_parser::{GlyphId, OutlineBuilder};
use std::cell::RefCell;
use std::rc::Rc;
use std::{collections::HashMap, ops::Deref, sync::Arc};

use crate::svg_font_document::SvgDocument;
use crate::{FontFace, FontFamily};
/// A wrapper of fontdb and cache font data.
pub struct FontDB {
Expand All @@ -13,12 +17,18 @@ pub struct FontDB {
cache: HashMap<ID, Option<Face>>,
}

type FontGlyphCache<K, V> = Rc<RefCell<HashMap<K, Option<V>>>>;
#[derive(Clone)]
pub struct Face {
pub face_id: ID,
pub source_data: Arc<dyn AsRef<[u8]> + Sync + Send>,
pub face_data_index: u32,
pub rb_face: rustybuzz::Face<'static>,
#[cfg(feature = "raster_png_font")]
raster_image_glyphs: FontGlyphCache<GlyphId, ShareResource<PixelImage>>,
outline_glyphs: FontGlyphCache<GlyphId, lyon_path::Path>,
svg_glyphs: FontGlyphCache<GlyphId, Svg>,
svg_docs: FontGlyphCache<*const u8, SvgDocument>,
}

impl FontDB {
Expand Down Expand Up @@ -263,6 +273,11 @@ impl Face {
face_data_index: face_index,
rb_face,
face_id,
outline_glyphs: <_>::default(),
#[cfg(feature = "raster_png_font")]
raster_image_glyphs: <_>::default(),
svg_glyphs: <_>::default(),
svg_docs: <_>::default(),
})
}

Expand All @@ -273,35 +288,65 @@ impl Face {

// todo: should return its tight bounds
pub fn outline_glyph(&self, glyph_id: GlyphId) -> Option<lyon_path::Path> {
let mut builder = GlyphOutlineBuilder::default();
let rect = self
.rb_face
.outline_glyph(glyph_id, &mut builder as &mut dyn OutlineBuilder);
rect.map(move |_| builder.into_path())
self
.outline_glyphs
.borrow_mut()
.entry(glyph_id)
.or_insert_with(|| {
let mut builder = GlyphOutlineBuilder::default();
let rect = self
.rb_face
.outline_glyph(glyph_id, &mut builder as &mut dyn OutlineBuilder);
rect.map(move |_| builder.into_path())
})
.as_ref()
.cloned()
}

#[cfg(feature = "raster_png_font")]
pub fn glyph_raster_image(&self, glyph_id: GlyphId, pixels_per_em: u16) -> Option<PixelImage> {
pub fn glyph_raster_image(
&self,
glyph_id: GlyphId,
pixels_per_em: u16,
) -> Option<ShareResource<PixelImage>> {
use rustybuzz::ttf_parser::RasterImageFormat;
self
.rb_face
.glyph_raster_image(glyph_id, pixels_per_em)
.and_then(|img| {
if img.format == RasterImageFormat::PNG {
Some(PixelImage::from_png(img.data))
} else {
None
}
.raster_image_glyphs
.borrow_mut()
.entry(glyph_id)
.or_insert_with(|| {
self
.rb_face
.glyph_raster_image(glyph_id, pixels_per_em)
.and_then(|img| {
if img.format == RasterImageFormat::PNG {
Some(ShareResource::new(PixelImage::from_png(img.data)))
} else {
None
}
})
})
.clone()
}

pub fn glyph_svg_image(&self, _: GlyphId) -> Option<Svg> {
None
// todo: need to extract glyph svg image, but the svg parse cost too long.
// self
// .rb_face
// .glyph_svg_image(glyph_id)
// .and_then(|data| Svg::parse_from_bytes(data).ok())
pub fn glyph_svg_image(&self, glyph_id: GlyphId) -> Option<Svg> {
self
.svg_glyphs
.borrow_mut()
.entry(glyph_id)
.or_insert_with(|| {
self.rb_face.glyph_svg_image(glyph_id).and_then(|data| {
self
.svg_docs
.borrow_mut()
.entry(&data[0] as *const u8)
.or_insert_with(|| SvgDocument::parse(unsafe { std::str::from_utf8_unchecked(data) }))
.as_ref()
.and_then(|doc| doc.glyph_svg(glyph_id, self))
.and_then(|content| Svg::parse_from_bytes(content.as_bytes()).ok())
})
})
.clone()
}

#[inline]
Expand Down
1 change: 1 addition & 0 deletions text/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ mod typography_store;
pub use typography_store::{TypographyStore, VisualGlyphs};
mod text_render;
pub use text_render::{draw_glyphs, draw_glyphs_in_rect, TextStyle};
mod svg_font_document;

mod text_writer;
pub use text_writer::{
Expand Down
231 changes: 231 additions & 0 deletions text/src/svg_font_document.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
use log::warn;
use quick_xml::events::attributes::Attribute;
use quick_xml::events::{BytesStart, Event};
use quick_xml::name::QName;
use quick_xml::reader::Reader;
use rustybuzz::ttf_parser::GlyphId;

use std::borrow::Cow;
use std::collections::HashMap;
use std::collections::HashSet;
use std::io::prelude::*;

use crate::font_db::Face;

pub(crate) struct SvgDocument {
elems: HashMap<String, String>,
}

impl SvgDocument {
pub(crate) fn parse(content: &str) -> Option<Self> {
let mut reader = Reader::from_str(content);
let mut buf = Vec::new();
let mut doc = Self { elems: HashMap::new() };
loop {
match reader.read_event_into(&mut buf) {
Ok(ref e @ Event::Start(ref tag)) | Ok(ref e @ Event::Empty(ref tag)) => {
if tag.name() != QName(b"defs") {
let has_child = matches!(e, Event::Start(_));
doc.collect_named_obj(&mut reader, content, tag, has_child);
}
}
Ok(Event::Eof) => break, // exits the loop when reaching end of file
Err(e) => {
warn!("Error at position {}: {:?}", reader.buffer_position(), e);
return None;
}

_ => (), // There are several other `Event`s we do not consider here
}
}
Some(doc)
}

pub fn glyph_svg(&self, glyph: GlyphId, face: &Face) -> Option<String> {
let key = format!("glyph{}", glyph.0);
if !self.elems.contains_key(&key) {
return None;
}

let mut all_links = HashSet::new();
let mut elems = vec![key.clone()];

while let Some(curr) = elems.pop() {
if let Some(content) = self.elems.get(&curr) {
elems.extend(Self::collect_link(content, &mut all_links));
}
}

let units_per_em = face.units_per_em() as i32;
let ascender = face.rb_face.ascender() as i32;
let mut writer = std::io::Cursor::new(Vec::new());

writer.write_all(format!(
"<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" version=\"1.1\" width=\"{}\" height=\"{}\" viewBox=\"{},{},{},{}\">",
units_per_em, units_per_em,
0, -ascender, units_per_em, units_per_em
).as_bytes()).ok()?;
writer.write_all("<defs>".as_bytes()).ok()?;
for link in all_links {
if let Some(content) = self.elems.get(&link) {
writer.write_all(content.as_bytes()).ok()?;
}
}
writer.write_all("</defs>".as_bytes()).ok()?;
writer
.write_all(self.elems.get(&key).unwrap().as_bytes())
.ok()?;
writer.write_all("</svg>".as_bytes()).ok()?;

Some(
std::str::from_utf8(&writer.into_inner())
.unwrap()
.to_string(),
)
}

fn collect_named_obj(
&mut self,
reader: &mut Reader<&[u8]>,
source: &str,
e: &BytesStart,
has_children: bool,
) {
if let Some(id) = e
.attributes()
.find(|a| a.as_ref().map_or(false, |a| a.key == QName(b"id")))
.map(|a| a.unwrap().value)
{
unsafe {
let content = Self::extra_elem(reader, e, source, has_children);
self
.elems
.insert(std::str::from_utf8_unchecked(&id).to_string(), content);
}
};
}

unsafe fn extra_elem(
reader: &mut Reader<&[u8]>,
e: &BytesStart,
source: &str,
has_children: bool,
) -> String {
let content = if has_children {
let mut buf = Vec::new();
let rg = reader
.read_to_end_into(e.name().to_owned(), &mut buf)
.unwrap();
&source[rg.start..rg.end]
} else {
""
};

let name = e.name();
let name = reader.decoder().decode(name.as_ref()).unwrap();

format!(
"<{}>{}</{}>",
std::str::from_utf8_unchecked(e),
content,
name
)
}

fn collect_link(content: &str, all_links: &mut HashSet<String>) -> Vec<String> {
let mut reader = Reader::from_str(content);
let mut buf = Vec::new();
let mut new_links = Vec::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(ref e)) | Ok(Event::Empty(ref e)) => {
Self::collect_link_from_attrs(e, all_links, &mut new_links);
}
Ok(Event::Eof) => break, // exits the loop when reaching end of file
Err(e) => panic!("Error at position {}: {:?}", reader.buffer_position(), e),

_ => (), // There are several other `Event`s we do not consider here
}
}
new_links
}

#[inline]
fn extra_link_from_iri_func(val: Cow<'_, [u8]>) -> Option<String> {
let val: &str = std::str::from_utf8(&val)
.unwrap()
.trim()
.strip_prefix("url(")?
.trim_start()
.strip_prefix('#')?
.strip_suffix(')')?;
Some(val.to_string())
}

#[inline]
fn extra_link_from_href(attr: &Attribute) -> Option<String> {
if attr.key == QName(b"xlink:href") || attr.key == QName(b"href") {
let href = std::str::from_utf8(&attr.value).unwrap();
return Some(href.trim().strip_prefix('#')?.to_string());
}
None
}

fn collect_link_from_attrs(
elem: &BytesStart,
all_links: &mut HashSet<String>,
new_links: &mut Vec<String>,
) {
let attributes = elem.attributes();

attributes.for_each(|attr| {
let attr = attr.unwrap();
if let Some(link) =
Self::extra_link_from_href(&attr).or_else(|| Self::extra_link_from_iri_func(attr.value))
{
if all_links.contains(&link) {
return;
}
all_links.insert(link.clone());
new_links.push(link);
}
});
}
}

#[cfg(test)]
mod tests {
use rustybuzz::ttf_parser::GlyphId;

use crate::font_db::FontDB;

#[test]
fn test_svg_document() {
let content = r##"
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1">
<defs>
<path
d="M262,-672 Q222,-610 216,-563 Q210,-516 237,-500 Q250,-493 262,-501 Q274,-509 284.5,-525 Q295,-541 303.5,-558.5 Q312,-576 319,-586 Q399,-705 535,-749 Q545,-753 556.5,-758 Q568,-763 573,-773 Q579,-785 572.5,-794.5 Q566,-804 554,-808 Q540,-814 522.5,-813.5 Q505,-813 488,-810 Q417,-798 355,-759.5 Q293,-721 262,-672 Z"
id="u1F250.2"></path>
<path
d="M393,25 Q393,-4 372.5,-24 Q352,-44 324,-44 Q296,-44 276,-24 Q256,-4 256,25 Q256,53 276,73 Q296,93 324,93 Q352,93 372.5,73 Q393,53 393,25 Z"
id="u1F69E.17"></path>
<radialGradient id="g799" cx="638" cy="380" r="508" gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1 0 0 0.525 0 0)">
<stop offset="0.598" stop-color="#212121" />
<stop offset="1" stop-color="#616161" />
</radialGradient>
</defs>
<g id="glyph2428">
<use xlink:href="#u1F69E.17" x="-1886.951" y="-548.858"
transform="matrix(7.674 0 0 7.674 12593.511 3663.078)" fill="#FFCC32" />
</g>
</svg>"##;
let doc = super::SvgDocument::parse(content).unwrap();
let mut db = FontDB::default();
let dummy_face = db.face_data_or_insert(db.default_font()).unwrap();
assert_eq!(doc.elems.len(), 4);
assert!(doc.glyph_svg(GlyphId(2428), dummy_face).is_some());
assert!(doc.glyph_svg(GlyphId(0), dummy_face).is_none());
}
}
Loading

0 comments on commit 17e6785

Please sign in to comment.