diff --git a/NEWS.md b/NEWS.md index c05b935..8271177 100644 --- a/NEWS.md +++ b/NEWS.md @@ -14,7 +14,7 @@ * `string2path()` now generates the same outline as `string2fill()` and `string2stroke()` (#69). -* `path_id` is now 1-origin. +* `path_id` and `glyph_id` are now 1-origin. # string2path 0.1.8 diff --git a/R/main.R b/R/main.R index 24ef3c7..252ba8b 100644 --- a/R/main.R +++ b/R/main.R @@ -14,7 +14,7 @@ #' @param tolerance Maximum distance allowed between the curve and its #' approximation. For more details, please refer to [the documentation of the #' underlying Rust -#' library](https://docs.rs/lyon/0.17.5/lyon/#what-is-the-tolerance-variable-in-these-examples). +#' library](https://docs.rs/lyon_geom/latest/lyon_geom/#flattening). #' #' @param line_width Line width of strokes. #' diff --git a/README.md b/README.md index 2470723..53e3890 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ +# string2path - # string2path @@ -9,16 +9,17 @@ [![Lifecycle: experimental](https://img.shields.io/badge/lifecycle-experimental-orange.svg)](https://lifecycle.r-lib.org/articles/stages.html#experimental) [![CRAN -status](https://www.r-pkg.org/badges/version/string2path)](https://CRAN.R-project.org/package=string2path) +status](https://www.r-pkg.org/badges/version/string2path.png)](https://CRAN.R-project.org/package=string2path) [![string2path status -badge](https://yutannihilation.r-universe.dev/badges/string2path)](https://yutannihilation.r-universe.dev) +badge](https://yutannihilation.r-universe.dev/badges/string2path.png)](https://yutannihilation.r-universe.dev) + The string2path R package converts a text to paths of the outlines of each glyph, based on a font data. Under the hood, this package is powered by [the savvy -framework](https://yutannihilation.github.io/savvy/guide/) to use -these two Rust crates: +framework](https://yutannihilation.github.io/savvy/guide/) to use these +two Rust crates: - [ttf-parser](https://github.com/RazrFalcon/ttf-parser) for parsing font data. TrueType font (`.ttf`) and OpenType font (`.otf`) are @@ -72,7 +73,7 @@ ggplot(d) + scale_colour_viridis_d(option = "H") ``` - + ``` r @@ -88,7 +89,7 @@ ggplot(d) + transition_reveal(rowid) ``` - + #### `dump_fontdb()` @@ -101,7 +102,7 @@ style (e.g. `"italic"`). ``` r dump_fontdb() #> # A tibble: 448 × 5 -#> x y family weight style +#> source index family weight style #> #> 1 "C:\\WINDOWS\\Fonts\\arial.ttf" 0 Arial normal normal #> 2 "C:\\WINDOWS\\Fonts\\arialbd.ttf" 0 Arial bold normal @@ -139,7 +140,7 @@ ggplot(d_tmp) + coord_equal() ``` - + ### `string2fill()` @@ -155,7 +156,7 @@ ggplot(d) + scale_fill_viridis_d(option = "H") ``` - + ### `string2stroke()` @@ -174,12 +175,15 @@ for (w in 1:9 * 0.01) { } ``` - + ## `tolerance` `tolerance` controls resolution of the tessellation. You can reduce -tolerance to get higher resolutions. +tolerance to get higher resolutions. In most of the cases, `1e-5` ~ +`1e-6` should be enough. For more details, please refer to [lyon’s +official +document](https://docs.rs/lyon_geom/latest/lyon_geom/#flattening). ``` r for (tolerance in c(1e-1, 1e-2, 1e-3, 1e-4, 1e-5, 1e-6, 1e-7)) { @@ -195,24 +199,4 @@ for (tolerance in c(1e-1, 1e-2, 1e-3, 1e-4, 1e-5, 1e-6, 1e-7)) { } ``` - - -Note that `tolerance` parameter behaves a bit differently on -`string2fill()` and `string2stroke()`. But, in either case, 1e-5 ~ 1e-6 -should be enough. - -``` r -for (tolerance in c(1e-1, 1e-2, 1e-3, 1e-4, 1e-5, 1e-6, 1e-7)) { - d <- string2path("abc", "Iosevka SS08", font_weight = "bold", font_style = "italic", tolerance = tolerance) - - p <- ggplot(d) + - geom_path(aes(x, y, group = path_id), colour = "black", linewidth = 0.5) + - geom_point(aes(x, y, group = path_id), colour = "black", size = 1.5) + - theme_minimal() + - coord_equal() + - ggtitle(paste0("tolerance: ", tolerance)) - plot(p) -} -``` - - + diff --git a/README.Rmd b/README.qmd similarity index 63% rename from README.Rmd rename to README.qmd index 939f920..432147d 100644 --- a/README.Rmd +++ b/README.qmd @@ -1,10 +1,12 @@ --- -output: github_document +title: "string2path" +format: gfm +editor: visual --- - - -```{r, include = FALSE} +```{r} +#| label: "setup" +#| include: false knitr::opts_chunk$set( collapse = TRUE, comment = "#>", @@ -18,19 +20,15 @@ knitr::opts_chunk$set( # string2path -[![R-CMD-check](https://github.com/yutannihilation/string2path/workflows/R-CMD-check/badge.svg)](https://github.com/yutannihilation/string2path/actions) -[![Lifecycle: experimental](https://img.shields.io/badge/lifecycle-experimental-orange.svg)](https://lifecycle.r-lib.org/articles/stages.html#experimental) -[![CRAN status](https://www.r-pkg.org/badges/version/string2path)](https://CRAN.R-project.org/package=string2path) -[![string2path status badge](https://yutannihilation.r-universe.dev/badges/string2path)](https://yutannihilation.r-universe.dev) - -The string2path R package converts a text to paths of the outlines of each glyph, based on a font data. -Under the hood, this package is powered by [the savvy framework][savvy] to use these two Rust crates: +[![R-CMD-check](https://github.com/yutannihilation/string2path/workflows/R-CMD-check/badge.svg)](https://github.com/yutannihilation/string2path/actions) [![Lifecycle: experimental](https://img.shields.io/badge/lifecycle-experimental-orange.svg)](https://lifecycle.r-lib.org/articles/stages.html#experimental) [![CRAN status](https://www.r-pkg.org/badges/version/string2path)](https://CRAN.R-project.org/package=string2path) [![string2path status badge](https://yutannihilation.r-universe.dev/badges/string2path)](https://yutannihilation.r-universe.dev) -* [ttf-parser](https://github.com/RazrFalcon/ttf-parser) for parsing font data. TrueType font (`.ttf`) and OpenType font (`.otf`) are supported. -* [lyon](https://github.com/nical/lyon/) for tessellation of polygons and flattening the curves. + + +The string2path R package converts a text to paths of the outlines of each glyph, based on a font data. Under the hood, this package is powered by [the savvy framework](https://yutannihilation.github.io/savvy/guide/) to use these two Rust crates: -[savvy]: https://yutannihilation.github.io/savvy/guide/ +- [ttf-parser](https://github.com/RazrFalcon/ttf-parser) for parsing font data. TrueType font (`.ttf`) and OpenType font (`.otf`) are supported. +- [lyon](https://github.com/nical/lyon/) for tessellation of polygons and flattening the curves. ## Installation @@ -51,9 +49,7 @@ install.packages("string2path", ) ``` -If you want to install from source, you need to have Rust toolchain installed -before trying to install this package. See -for the installation instructions. +If you want to install from source, you need to have Rust toolchain installed before trying to install this package. See for the installation instructions. ## Example @@ -91,19 +87,14 @@ ggplot(d) + #### `dump_fontdb()` -Note that `"Noto Sans JP"` above (and `"Iosevka SS08"` below) is the font installed -on my local machine, so the same code might not run on your environment. You can -use `dump_fontdb()` to see the available combination of font family (e.g. `"Arial"`), -weight (e.g. `"bold"`), and style (e.g. `"italic"`). +Note that `"Noto Sans JP"` above (and `"Iosevka SS08"` below) is the font installed on my local machine, so the same code might not run on your environment. You can use `dump_fontdb()` to see the available combination of font family (e.g. `"Arial"`), weight (e.g. `"bold"`), and style (e.g. `"italic"`). ```{r} #| label: dump dump_fontdb() ``` -You can also specify the font file directly. Pomicons is a font available on -[gabrielelana/pomicons](https://github.com/gabrielelana/pomicons), licensed under -SIL OFL 1.1. +You can also specify the font file directly. Pomicons is a font available on [gabrielelana/pomicons](https://github.com/gabrielelana/pomicons), licensed under SIL OFL 1.1. ```{r} #| label: icon_font @@ -124,7 +115,6 @@ ggplot(d_tmp) + coord_equal() ``` - ### `string2fill()` ```{r} @@ -159,11 +149,9 @@ for (w in 1:9 * 0.01) { } ``` - - ## `tolerance` -`tolerance` controls resolution of the tessellation. You can reduce tolerance to get higher resolutions. +`tolerance` controls resolution of the tessellation. You can reduce tolerance to get higher resolutions. In most of the cases, `1e-5` \~ `1e-6` should be enough. For more details, please refer to [lyon's official document](https://docs.rs/lyon_geom/latest/lyon_geom/#flattening). ```{r} #| label: example3 @@ -180,23 +168,3 @@ for (tolerance in c(1e-1, 1e-2, 1e-3, 1e-4, 1e-5, 1e-6, 1e-7)) { plot(p) } ``` - -Note that `tolerance` parameter behaves a bit differently on `string2fill()` and `string2stroke()`. -But, in either case, 1e-5 ~ 1e-6 should be enough. - -```{r} -#| label: example4 -#| animation.hook: gifski -for (tolerance in c(1e-1, 1e-2, 1e-3, 1e-4, 1e-5, 1e-6, 1e-7)) { - d <- string2path("abc", "Iosevka SS08", font_weight = "bold", font_style = "italic", tolerance = tolerance) - - p <- ggplot(d) + - geom_path(aes(x, y, group = path_id), colour = "black", linewidth = 0.5) + - geom_point(aes(x, y, group = path_id), colour = "black", size = 1.5) + - theme_minimal() + - coord_equal() + - ggtitle(paste0("tolerance: ", tolerance)) - plot(p) -} -``` - diff --git a/man/figures/README-example-1.gif b/man/figures/README-example-1.gif index 36898ca..63f29c5 100644 Binary files a/man/figures/README-example-1.gif and b/man/figures/README-example-1.gif differ diff --git a/man/figures/README-example-1.png b/man/figures/README-example-1.png index 021d667..a44bea4 100644 Binary files a/man/figures/README-example-1.png and b/man/figures/README-example-1.png differ diff --git a/man/figures/README-example2-1.png b/man/figures/README-example2-1.png index 9c6b329..ccffdf8 100644 Binary files a/man/figures/README-example2-1.png and b/man/figures/README-example2-1.png differ diff --git a/man/figures/README-example3-.gif b/man/figures/README-example3-.gif index 5951f53..961aaf2 100644 Binary files a/man/figures/README-example3-.gif and b/man/figures/README-example3-.gif differ diff --git a/man/figures/README-icon_font-1.png b/man/figures/README-icon_font-1.png index 875ee87..8269e72 100644 Binary files a/man/figures/README-icon_font-1.png and b/man/figures/README-icon_font-1.png differ diff --git a/man/figures/README-string2stroke-.gif b/man/figures/README-string2stroke-.gif index f51d19f..1e17aef 100644 Binary files a/man/figures/README-string2stroke-.gif and b/man/figures/README-string2stroke-.gif differ diff --git a/man/string2path.Rd b/man/string2path.Rd index 40ec842..b2e4b40 100644 --- a/man/string2path.Rd +++ b/man/string2path.Rd @@ -45,7 +45,7 @@ string2fill( \item{font_style}{A font style.} \item{tolerance}{Maximum distance allowed between the curve and its -approximation. For more details, please refer to \href{https://docs.rs/lyon/0.17.5/lyon/#what-is-the-tolerance-variable-in-these-examples}{the documentation of the underlying Rust library}.} +approximation. For more details, please refer to \href{https://docs.rs/lyon_geom/latest/lyon_geom/#flattening}{the documentation of the underlying Rust library}.} \item{line_width}{Line width of strokes.} } diff --git a/src/rust/src/builder.rs b/src/rust/src/builder.rs index 21d4c11..e6d2eb9 100644 --- a/src/rust/src/builder.rs +++ b/src/rust/src/builder.rs @@ -53,6 +53,23 @@ pub struct LyonPathBuilder { } impl LyonPathBuilder { + fn new_inner(builder: T, tolerance: f32, line_width: f32) -> Self { + Self { + builders: vec![builder], + layer_color: HashMap::new(), + cur_layer: 0, + cur_glyph_id: 0, + cur_path_id: 0, + glyph_id_map: HashMap::new(), + base_transform: lyon::geom::euclid::Transform2D::identity(), + scale_factor: 1., + offset_x: 0., + offset_y: 0., + tolerance, + line_width, + } + } + #[inline] pub fn cur_builder(&mut self) -> &mut T { &mut self.builders[self.cur_layer] @@ -71,15 +88,6 @@ impl LyonPathBuilder { .collect() } - // adds offsets to x and y - pub fn point(&self, x: f32, y: f32) -> lyon::math::Point { - point(x, y) - } - - pub fn ids(&self) -> [f32; 2] { - [self.cur_glyph_id as _, self.cur_path_id as _] - } - pub fn update_transform(&mut self) { let transform = self .base_transform @@ -151,18 +159,20 @@ impl LyonPathBuilder { // For path -pub type FlattenedPathBuilder = lyon::path::builder::Transformed< - lyon::path::builder::Flattened, - lyon::math::Transform, +pub type FlattenedPathBuilder = lyon::path::builder::NoAttributes< + lyon::path::builder::Transformed< + lyon::path::builder::Flattened, + lyon::math::Transform, + >, >; impl BuildPath for FlattenedPathBuilder { fn set_transform(&mut self, transform: lyon::math::Transform) { - self.set_transform(transform); + self.inner_mut().set_transform(transform); } fn new_builder(tolerance: f32) -> Self { - lyon::path::Path::builder_with_attributes(2) + lyon::path::Path::builder() .flattened(tolerance) .transformed(lyon::geom::euclid::Transform2D::identity()) } @@ -173,38 +183,23 @@ pub type LyonPathBuilderForPath = LyonPathBuilder; impl LyonPathBuilderForPath { pub fn new(tolerance: f32, line_width: f32) -> Self { let builder = FlattenedPathBuilder::new_builder(tolerance); - Self { - builders: vec![builder], - layer_color: HashMap::new(), - cur_layer: 0, - cur_glyph_id: 0, - cur_path_id: 0, - glyph_id_map: HashMap::new(), - base_transform: lyon::geom::euclid::Transform2D::identity(), - scale_factor: 1., - offset_x: 0., - offset_y: 0., - tolerance, - line_width, - } + Self::new_inner(builder, tolerance, line_width) } } // For stroke and fill -pub type NonFlattenedPathBuilder = lyon::path::builder::Transformed< - lyon::path::path::BuilderWithAttributes, - lyon::math::Transform, +pub type NonFlattenedPathBuilder = lyon::path::builder::NoAttributes< + lyon::path::builder::Transformed, >; impl BuildPath for NonFlattenedPathBuilder { fn set_transform(&mut self, transform: lyon::math::Transform) { - self.set_transform(transform); + self.inner_mut().set_transform(transform); } fn new_builder(_tolerance: f32) -> Self { - lyon::path::Path::builder_with_attributes(2) - .transformed(lyon::geom::euclid::Transform2D::identity()) + lyon::path::Path::builder().transformed(lyon::geom::euclid::Transform2D::identity()) } } @@ -213,20 +208,7 @@ pub type LyonPathBuilderForStrokeAndFill = LyonPathBuilder Self { let builder = NonFlattenedPathBuilder::new_builder(tolerance); - Self { - builders: vec![builder], - layer_color: HashMap::new(), - cur_layer: 0, - cur_glyph_id: 0, - cur_path_id: 0, - glyph_id_map: HashMap::new(), - base_transform: lyon::geom::euclid::Transform2D::identity(), - scale_factor: 1., - offset_x: 0., - offset_y: 0., - tolerance, - line_width, - } + Self::new_inner(builder, tolerance, line_width) } } @@ -242,32 +224,26 @@ impl ttf_parser::OutlineBuilder for LyonPathBuilder { self.glyph_id_map .insert(self.cur_path_id, self.cur_glyph_id); - let at = self.point(x, y); - let custom_attributes = &self.ids(); - self.cur_builder().begin(at, custom_attributes); + let at = point(x, y); + self.cur_builder().begin(at, &[]); } fn line_to(&mut self, x: f32, y: f32) { - let to = self.point(x, y); - let custom_attributes = &self.ids(); - self.cur_builder().line_to(to, custom_attributes); + let to = point(x, y); + self.cur_builder().line_to(to, &[]); } fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) { - let ctrl = self.point(x1, y1); - let to = self.point(x, y); - let custom_attributes = &self.ids(); - self.cur_builder() - .quadratic_bezier_to(ctrl, to, custom_attributes); + let ctrl = point(x1, y1); + let to = point(x, y); + self.cur_builder().quadratic_bezier_to(ctrl, to, &[]); } fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) { - let ctrl1 = self.point(x1, y1); - let ctrl2 = self.point(x2, y2); - let to = self.point(x, y); - let custom_attributes = &self.ids(); - self.cur_builder() - .cubic_bezier_to(ctrl1, ctrl2, to, custom_attributes); + let ctrl1 = point(x1, y1); + let ctrl2 = point(x2, y2); + let to = point(x, y); + self.cur_builder().cubic_bezier_to(ctrl1, ctrl2, to, &[]); } fn close(&mut self) { diff --git a/src/rust/src/font.rs b/src/rust/src/font.rs index e767452..7b188b5 100644 --- a/src/rust/src/font.rs +++ b/src/rust/src/font.rs @@ -154,6 +154,10 @@ impl LyonPathBuilder { prev_glyph = None; continue; } + + // increment glyph ID for consistency + self.cur_glyph_id += 1; + // Even when we cannot find glyph_id, fill it with 0. let cur_glyph = font.glyph_index(c).unwrap_or(GlyphId(0)); @@ -176,7 +180,6 @@ impl LyonPathBuilder { } prev_glyph = Some(cur_glyph); - self.cur_glyph_id += 1; } Ok(()) diff --git a/src/rust/src/into_fill_stroke.rs b/src/rust/src/into_fill_stroke.rs index f8d7be7..2e896d3 100644 --- a/src/rust/src/into_fill_stroke.rs +++ b/src/rust/src/into_fill_stroke.rs @@ -4,37 +4,21 @@ use lyon::tessellation::*; use ttf_parser::RgbaColor; #[derive(Copy, Clone, Debug)] -struct Vertex { - position: lyon::math::Point, - glyph_id: u32, - path_id: u32, -} +struct Vertex(lyon::math::Point); // This can have some members so that it can be used in new_vertex(), but I // don't find any useful usage yet. struct VertexCtor {} impl FillVertexConstructor for VertexCtor { - fn new_vertex(&mut self, mut vertex: FillVertex) -> Vertex { - let pos = vertex.position(); - let attr = vertex.interpolated_attributes(); - Vertex { - position: pos, - glyph_id: attr[0] as _, - path_id: attr[1] as _, - } + fn new_vertex(&mut self, vertex: FillVertex) -> Vertex { + Vertex(vertex.position()) } } impl StrokeVertexConstructor for VertexCtor { - fn new_vertex(&mut self, mut vertex: StrokeVertex) -> Vertex { - let pos = vertex.position(); - let attr = vertex.interpolated_attributes(); - Vertex { - position: pos, - glyph_id: attr[0] as _, - path_id: attr[1] as _, - } + fn new_vertex(&mut self, vertex: StrokeVertex) -> Vertex { + Vertex(vertex.position()) } } @@ -51,7 +35,7 @@ impl LyonPathBuilderForStrokeAndFill { x: Vec::new(), y: Vec::new(), glyph_id: Vec::new(), - path_id: Vec::new(), + path_id: None, triangle_id: Some(Vec::new()), color, }; @@ -60,7 +44,14 @@ impl LyonPathBuilderForStrokeAndFill { let mut tessellator = FillTessellator::new(); let options = FillOptions::tolerance(self.tolerance); + let mut cur_path_id: u32 = 0; for (path, color) in paths { + let path_id_inc = path + .iter() + .filter(|x| matches!(x, path::Event::Begin { .. })) + .count(); + cur_path_id += path_id_inc as u32; + let mut geometry: VertexBuffers = VertexBuffers::new(); { // Compute the tessellation. @@ -72,7 +63,9 @@ impl LyonPathBuilderForStrokeAndFill { ) .unwrap(); } - extract_vertex_buffer(geometry, &mut result, color); + + let cur_glyph_id = *self.glyph_id_map.get(&cur_path_id).unwrap_or(&0) as i32; + extract_vertex_buffer(geometry, &mut result, color, cur_glyph_id); } result } @@ -89,15 +82,23 @@ impl LyonPathBuilderForStrokeAndFill { x: Vec::new(), y: Vec::new(), glyph_id: Vec::new(), - path_id: Vec::new(), + path_id: None, triangle_id: Some(Vec::new()), color, }; + let mut cur_path_id: u32 = 0; + // Will contain the result of the tessellation. let mut tessellator = StrokeTessellator::new(); let options = StrokeOptions::tolerance(self.tolerance).with_line_width(self.line_width); for (path, color) in paths { + let path_id_inc = path + .iter() + .filter(|x| matches!(x, path::Event::Begin { .. })) + .count(); + cur_path_id += path_id_inc as u32; + let mut geometry: VertexBuffers = VertexBuffers::new(); { // Compute the tessellation. @@ -110,7 +111,8 @@ impl LyonPathBuilderForStrokeAndFill { .unwrap(); } - extract_vertex_buffer(geometry, &mut result, color); + let cur_glyph_id = *self.glyph_id_map.get(&cur_path_id).unwrap_or(&0) as i32; + extract_vertex_buffer(geometry, &mut result, color, cur_glyph_id); } result } @@ -120,6 +122,7 @@ fn extract_vertex_buffer( geometry: VertexBuffers, dst: &mut PathTibble, paint_color: Option, + glyph_id: i32, ) { let offset = dst.triangle_id.as_ref().map_or(0, |v| match v.last() { Some(last_triangle_id) => last_triangle_id + 1, @@ -127,10 +130,9 @@ fn extract_vertex_buffer( }); for (n, &i) in geometry.indices.iter().enumerate() { if let Some(v) = geometry.vertices.get(i) { - dst.x.push(v.position.x as _); - dst.y.push(v.position.y as _); - dst.glyph_id.push(v.glyph_id as _); - dst.path_id.push(v.path_id as _); + dst.x.push(v.0.x as _); + dst.y.push(v.0.y as _); + dst.glyph_id.push(glyph_id); if let Some(triangle_id) = &mut dst.triangle_id { triangle_id.push(n as i32 / 3 + offset); } diff --git a/src/rust/src/into_path.rs b/src/rust/src/into_path.rs index 6edfe4a..bd9072a 100644 --- a/src/rust/src/into_path.rs +++ b/src/rust/src/into_path.rs @@ -6,19 +6,18 @@ use crate::result::PathTibble; impl LyonPathBuilderForPath { pub fn into_path(mut self) -> PathTibble { let paths = self.build(); - let color = if self.layer_color.is_empty() { + + let mut x = Vec::new(); + let mut y = Vec::new(); + let mut glyph_id = Vec::new(); + let mut path_id = Vec::new(); + let mut color = if self.layer_color.is_empty() { None } else { Some(Vec::new()) }; - let mut result = PathTibble { - x: Vec::new(), - y: Vec::new(), - glyph_id: Vec::new(), - path_id: Vec::new(), - triangle_id: None, - color, - }; + + let mut cur_path_id: u32 = 0; for (path, paint_color) in paths { let paint_color = match paint_color { Some(RgbaColor { @@ -29,7 +28,6 @@ impl LyonPathBuilderForPath { }) => format!("#{red:02x}{green:02x}{blue:02x}{alpha:02x}",), None => "#00000000".to_string(), }; - let mut cur_path_id: u32 = 0; for p in path.iter() { let point = match p { lyon::path::Event::Begin { at } => { @@ -50,19 +48,27 @@ impl LyonPathBuilderForPath { }; if let Some(pos) = point { - result.x.push(pos.x as _); - result.y.push(pos.y as _); - result - .glyph_id - .push(*self.glyph_id_map.get(&cur_path_id).unwrap_or(&0) as _); - result.path_id.push(cur_path_id as _); + x.push(pos.x as _); + y.push(pos.y as _); - if let Some(v) = result.color.as_mut() { + let cur_glyph_id = *self.glyph_id_map.get(&cur_path_id).unwrap_or(&0) as _; + glyph_id.push(cur_glyph_id); + path_id.push(cur_path_id as _); + + if let Some(v) = color.as_mut() { v.push(paint_color.clone()) } } } } - result + + PathTibble { + x, + y, + glyph_id, + path_id: Some(path_id), + triangle_id: None, + color, + } } } diff --git a/src/rust/src/result.rs b/src/rust/src/result.rs index e5e1edc..847bf9e 100644 --- a/src/rust/src/result.rs +++ b/src/rust/src/result.rs @@ -9,25 +9,34 @@ pub struct PathTibble { // IDs to distinguish the glyphs. Note that this is a different ID than [ttf_parser::GlyphId]. pub glyph_id: Vec, // IDs to distinguish the groups of paths (i.e., `Begin` path event to `End` path event). - pub path_id: Vec, + pub path_id: Option>, // IDs to distinguish the triangles. This field is `None` for `ConversionType::Path`. pub triangle_id: Option>, // Color of color emoji font. pub color: Option>, } -impl TryFrom for savvy::Sexp { - type Error = savvy::Error; - - fn try_from(value: PathTibble) -> savvy::Result { - let mut len = 4; - if value.triangle_id.is_some() { +impl PathTibble { + fn len(&self) -> usize { + let mut len = 3; + if self.path_id.is_some() { + len += 1 + }; + if self.triangle_id.is_some() { len += 1 }; - if value.color.is_some() { + if self.color.is_some() { len += 1 }; - let mut out = savvy::OwnedListSexp::new(len, true)?; + len + } +} + +impl TryFrom for savvy::Sexp { + type Error = savvy::Error; + + fn try_from(value: PathTibble) -> savvy::Result { + let mut out = savvy::OwnedListSexp::new(value.len(), true)?; out.set_name_and_value(0, "x", ::try_from(value.x.as_slice())?)?; out.set_name_and_value(1, "y", ::try_from(value.y.as_slice())?)?; @@ -36,24 +45,24 @@ impl TryFrom for savvy::Sexp { "glyph_id", ::try_from(value.glyph_id.as_slice())?, )?; - out.set_name_and_value( - 3, - "path_id", - ::try_from(value.path_id.as_slice())?, - )?; - let mut idx = 3; + // optional columns + let mut idx = 2; + + if let Some(path_id) = value.path_id { + idx += 1; + let v = ::try_from(path_id.as_slice())?; + out.set_name_and_value(idx, "path_id", v)?; + } if let Some(triangle_id) = value.triangle_id { idx += 1; - out.set_name_and_value( - idx, - "triangle_id", - ::try_from(triangle_id.as_slice())?, - )?; + let v = ::try_from(triangle_id.as_slice())?; + out.set_name_and_value(idx, "triangle_id", v)?; } if let Some(color) = value.color { idx += 1; - out.set_name_and_value(idx, "color", ::try_from(color.as_slice())?)?; + let v = ::try_from(color.as_slice())?; + out.set_name_and_value(idx, "color", v)?; } out.into() diff --git a/tests/testthat/_snaps/snapshot.md b/tests/testthat/_snaps/snapshot.md index 650216c..dea8003 100644 --- a/tests/testthat/_snaps/snapshot.md +++ b/tests/testthat/_snaps/snapshot.md @@ -6,49 +6,49 @@ # A tibble: 4 x 4 x y glyph_id path_id - 1 0 0 0 1 - 2 0.800 0.800 0 1 - 3 0 0.800 0 1 - 4 0 0 0 1 + 1 0 0 1 1 + 2 0.800 0.800 1 1 + 3 0 0.800 1 1 + 4 0 0 1 1 --- Code string2stroke("A", "./font/test.ttf") Output - # A tibble: 18 x 5 - x y glyph_id path_id triangle_id - - 1 0.836 0.815 0 1 0 - 2 0.764 0.785 0 1 0 - 3 0.0150 0.785 0 1 0 - 4 0.836 0.815 0 1 1 - 5 0.0150 0.785 0 1 1 - 6 -0.0150 0.815 0 1 1 - 7 -0.0150 0.815 0 1 2 - 8 0.0150 0.785 0 1 2 - 9 0.0150 0.0362 0 1 2 - 10 -0.0150 0.815 0 1 3 - 11 0.0150 0.0362 0 1 3 - 12 -0.0150 -0.0362 0 1 3 - 13 -0.0150 -0.0362 0 1 4 - 14 0.0150 0.0362 0 1 4 - 15 0.764 0.785 0 1 4 - 16 -0.0150 -0.0362 0 1 5 - 17 0.764 0.785 0 1 5 - 18 0.836 0.815 0 1 5 + # A tibble: 18 x 4 + x y glyph_id triangle_id + + 1 0.836 0.815 1 0 + 2 0.764 0.785 1 0 + 3 0.0150 0.785 1 0 + 4 0.836 0.815 1 1 + 5 0.0150 0.785 1 1 + 6 -0.0150 0.815 1 1 + 7 -0.0150 0.815 1 2 + 8 0.0150 0.785 1 2 + 9 0.0150 0.0362 1 2 + 10 -0.0150 0.815 1 3 + 11 0.0150 0.0362 1 3 + 12 -0.0150 -0.0362 1 3 + 13 -0.0150 -0.0362 1 4 + 14 0.0150 0.0362 1 4 + 15 0.764 0.785 1 4 + 16 -0.0150 -0.0362 1 5 + 17 0.764 0.785 1 5 + 18 0.836 0.815 1 5 --- Code string2fill("A", "./font/test.ttf") Output - # A tibble: 3 x 5 - x y glyph_id path_id triangle_id - - 1 0 0 0 1 0 - 2 0 0.800 0 1 0 - 3 0.800 0.800 0 1 0 + # A tibble: 3 x 4 + x y glyph_id triangle_id + + 1 0 0 1 0 + 2 0 0.800 1 0 + 3 0.800 0.800 1 0 # the data extracted from installed font are as expected @@ -58,16 +58,16 @@ # A tibble: 27 x 4 x y glyph_id path_id - 1 -0.00131 0 0 1 - 2 0.245 0.641 0 1 - 3 0.336 0.641 0 1 - 4 0.598 0 0 1 - 5 0.502 0 0 1 - 6 0.427 0.194 0 1 - 7 0.159 0.194 0 1 - 8 0.0887 0 0 1 - 9 -0.00131 0 0 1 - 10 0.184 0.263 0 2 + 1 -0.00131 0 1 1 + 2 0.245 0.641 1 1 + 3 0.336 0.641 1 1 + 4 0.598 0 1 1 + 5 0.502 0 1 1 + 6 0.427 0.194 1 1 + 7 0.159 0.194 1 1 + 8 0.0887 0 1 1 + 9 -0.00131 0 1 1 + 10 0.184 0.263 1 2 # i 17 more rows --- @@ -75,19 +75,19 @@ Code string2stroke("A", "Arial") Output - # A tibble: 150 x 5 - x y glyph_id path_id triangle_id - - 1 0.255 0.626 0 1 0 - 2 0.234 0.656 0 1 0 - 3 0.346 0.656 0 1 0 - 4 0.255 0.626 0 1 1 - 5 0.346 0.656 0 1 1 - 6 0.326 0.626 0 1 1 - 7 0.326 0.626 0 1 2 - 8 0.346 0.656 0 1 2 - 9 0.621 -0.0150 0 1 2 - 10 0.326 0.626 0 1 3 + # A tibble: 150 x 4 + x y glyph_id triangle_id + + 1 0.255 0.626 1 0 + 2 0.234 0.656 1 0 + 3 0.346 0.656 1 0 + 4 0.255 0.626 1 1 + 5 0.346 0.656 1 1 + 6 0.326 0.626 1 1 + 7 0.326 0.626 1 2 + 8 0.346 0.656 1 2 + 9 0.621 -0.0150 1 2 + 10 0.326 0.626 1 3 # i 140 more rows --- @@ -95,18 +95,18 @@ Code string2fill("A", "Arial") Output - # A tibble: 75 x 5 - x y glyph_id path_id triangle_id - - 1 0.0887 0 0 1 0 - 2 -0.00131 0 0 1 0 - 3 0.159 0.194 0 1 0 - 4 0.427 0.194 0 1 1 - 5 0.159 0.194 0 1 1 - 6 0.184 0.263 0 2 1 - 7 0.159 0.194 0 1 2 - 8 -0.00131 0 0 1 2 - 9 0.184 0.263 0 2 2 - 10 0.184 0.263 0 2 3 + # A tibble: 75 x 4 + x y glyph_id triangle_id + + 1 0.0887 0 1 0 + 2 -0.00131 0 1 0 + 3 0.159 0.194 1 0 + 4 0.427 0.194 1 1 + 5 0.159 0.194 1 1 + 6 0.184 0.263 1 1 + 7 0.159 0.194 1 2 + 8 -0.00131 0 1 2 + 9 0.184 0.263 1 2 + 10 0.184 0.263 1 3 # i 65 more rows