diff --git a/DESCRIPTION b/DESCRIPTION index 401820f..d150145 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -17,6 +17,7 @@ BugReports: https://github.com/r-lib/textshaping/issues Depends: R (>= 3.2.0) Imports: + lifecycle, systemfonts (>= 1.0.0) Suggests: covr, @@ -29,5 +30,6 @@ VignetteBuilder: knitr Encoding: UTF-8 Roxygen: list(markdown = TRUE) -RoxygenNote: 7.2.3 +RoxygenNote: 7.3.1 SystemRequirements: freetype2, harfbuzz, fribidi +Remotes: r-lib/systemfonts diff --git a/NAMESPACE b/NAMESPACE index ef8c4c8..5d9c6c5 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -3,6 +3,8 @@ export(get_font_features) export(shape_text) export(text_width) -importFrom(systemfonts,match_font) +importFrom(lifecycle,deprecated) +importFrom(systemfonts,font_feature) +importFrom(systemfonts,match_fonts) importFrom(systemfonts,system_fonts) useDynLib(textshaping, .registration = TRUE) diff --git a/NEWS.md b/NEWS.md index a5e2982..323751e 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,11 +1,7 @@ # textshaping (development version) -* Fixed height calculation in `shape_text()` to avoid an extra line of height -* Fixed width calculation in `shape_text()` to ignore width of terminal - line-break characters -* Added `'justified'` and `'distributed'` options to `align` in `shape_text()` - to either expand spaces to fit line width to full width, or distribute all - glyphs to fit line width to full width +* Full rewrite of `shape_text()` to allow proper font-fallback, bidi text + support, support for font-features, spacers, new align settings, etc. # textshaping 0.3.7 diff --git a/R/cpp11.R b/R/cpp11.R index 7f44f6e..f10c8e8 100644 --- a/R/cpp11.R +++ b/R/cpp11.R @@ -4,8 +4,8 @@ get_face_features_c <- function(path, index) { .Call(`_textshaping_get_face_features_c`, path, index) } -get_string_shape_c <- function(string, id, path, index, size, res, lineheight, align, hjust, vjust, width, tracking, indent, hanging, space_before, space_after) { - .Call(`_textshaping_get_string_shape_c`, string, id, path, index, size, res, lineheight, align, hjust, vjust, width, tracking, indent, hanging, space_before, space_after) +get_string_shape_c <- function(string, id, path, index, features, size, res, lineheight, align, hjust, vjust, width, tracking, indent, hanging, space_before, space_after) { + .Call(`_textshaping_get_string_shape_c`, string, id, path, index, features, size, res, lineheight, align, hjust, vjust, width, tracking, indent, hanging, space_before, space_after) } get_line_width_c <- function(string, path, index, size, res, include_bearing) { diff --git a/R/font_features.R b/R/font_features.R index 8961aa0..8e165dc 100644 --- a/R/font_features.R +++ b/R/font_features.R @@ -9,7 +9,7 @@ #' @return A list with an element for each of the input fonts containing the #' supported feature tags for that font. #' -#' @importFrom systemfonts match_font +#' @importFrom systemfonts match_fonts #' @export #' #' @examples @@ -24,18 +24,13 @@ get_font_features <- function(family = '', italic = FALSE, bold = FALSE, path = NULL, index = 0) { if (is.null(path)) { full_length <- max(length(family), length(italic), length(bold)) - if (full_length == 1) { - loc <- match_font(family, italic, bold) - path <- loc$path - index <- loc$index - } else { - family <- rep_len(family, full_length) - italic <- rep_len(italic, full_length) - bold <- rep_len(bold, full_length) - loc <- Map(match_font, family = family, italic = italic, bold = bold) - path <- vapply(loc, `[[`, character(1L), 1, USE.NAMES = FALSE) - index <- vapply(loc, `[[`, integer(1L), 2, USE.NAMES = FALSE) - } + fonts <- match_fonts( + rep_len(family, full_length), + rep_len(italic, full_length), + ifelse(rep_len(bold, full_length), "bold", "normal") + ) + path <- fonts$path + index <- fonts$index } else { full_length <- max(length(path), length(index)) path <- rep_len(path, full_length) diff --git a/R/shape_text.R b/R/shape_text.R index 16599d6..842af5a 100644 --- a/R/shape_text.R +++ b/R/shape_text.R @@ -12,12 +12,17 @@ #' @param strings A character vector of strings to shape #' @param id A vector grouping the strings together. If strings share an id the #' shaping will continue between strings -#' @inheritParams systemfonts::font_info +#' @inheritParams systemfonts::match_fonts +#' @param features A [systemfonts::font_feature()] object or a list of them, +#' giving the OpenType font features to set +#' @param size The size in points to use for the font +#' @param res The resolution to use when doing the shaping. Should optimally +#' match the resolution used when rendering the glyphs. #' @param lineheight A multiplier for the lineheight #' @param align Within text box alignment, either `'left'`, `'center'`, `'right'`, -#' `'justified'`, or `'distributed'` +#' `'justified-left'`, `'justified-right'`, `'justified-center'`, or `'distributed'` #' @param hjust,vjust The justification of the textbox surrounding the text -#' @param width The requested with of the string in inches. Setting this to +#' @param max_width The requested with of the string in inches. Setting this to #' something other than `NA` will turn on word wrapping. #' @param tracking Tracking of the glyphs (space adjustment) measured in 1/1000 #' em. @@ -61,6 +66,7 @@ #' } #' #' @export +#' @importFrom systemfonts font_feature match_fonts #' #' @examples #' string <- "This is a long string\nLook; It spans multiple lines\nand all" @@ -78,10 +84,11 @@ #' shape_text(string, id = c(1, 1, 1), size = c(12, 24, 12)) #' shape_text <- function(strings, id = NULL, family = '', italic = FALSE, - bold = FALSE, size = 12, res = 72, lineheight = 1, - align = 'left', hjust = 0, vjust = 0, width = NA, - tracking = 0, indent = 0, hanging = 0, space_before = 0, - space_after = 0, path = NULL, index = 0) { + weight = 'normal', width = 'normal', features = font_feature(), + size = 12, res = 72, lineheight = 1, align = 'left', + hjust = 0, vjust = 0, max_width = NA, tracking = 0, + indent = 0, hanging = 0, space_before = 0, space_after = 0, + path = NULL, index = 0, bold = deprecated()) { n_strings = length(strings) if (is.null(id)) id <- seq_len(n_strings) id <- rep_len(id, n_strings) @@ -93,62 +100,72 @@ shape_text <- function(strings, id = NULL, family = '', italic = FALSE, id <- id[ido] strings <- as.character(strings)[ido] + if (lifecycle::is_present(bold)) { + lifecycle::deprecate_soft("0.4.0", "shape_text(bold)", "shape_text(weight='bold')") + weight <- ifelse(bold, "bold", "normal") + } + + if (inherits(features, 'font_feature')) features <- list(features) + features <- rep_len(features, n_strings) + if (is.null(path)) { - if (all(c(length(family), length(italic), length(bold)) == 1)) { - loc <- systemfonts::match_font(family, italic, bold) - path <- loc$path - index <- loc$index - } else { - family <- rep_len(family, n_strings) - italic <- rep_len(italic, n_strings) - bold <- rep_len(bold, n_strings) - loc <- Map(systemfonts::match_font, family = family, italic = italic, bold = bold) - path <- vapply(loc, `[[`, character(1L), 1, USE.NAMES = FALSE)[ido] - index <- vapply(loc, `[[`, integer(1L), 2, USE.NAMES = FALSE)[ido] - } + family <- rep_len(family, n_strings) + italic <- rep_len(italic, n_strings) + weight <- rep_len(weight, n_strings) + width <- rep_len(width, n_strings) + loc <- match_fonts(family, italic, weight, width) + path <- loc$path[ido] + index <- loc$index[ido] + features <- Map(c, loc$features, features)[ido] } else { - if (!all(c(length(path), length(index)) == 1)) { - path <- rep_len(path, n_strings)[ido] - index <- rep_len(index, n_strings)[ido] - } + path <- rep_len(path, n_strings)[ido] + index <- rep_len(index, n_strings)[ido] + features <- features[ido] } - if (length(size) != 1) size <- rep_len(size, n_strings)[ido] - if (length(res) != 1) res <- rep_len(res, n_strings)[ido] - if (length(lineheight) != 1) lineheight <- rep_len(lineheight, n_strings)[ido] - align <- match.arg(align, c('left', 'center', 'right', 'justified', 'distributed'), TRUE) - align <- match(align, c('left', 'center', 'right', 'justified', 'distributed')) - if (length(align) != 1) align <- rep_len(align, n_strings)[ido] - if (length(hjust) != 1) hjust <- rep_len(hjust, n_strings)[ido] - if (length(vjust) != 1) vjust <- rep_len(vjust, n_strings)[ido] - if (length(width) != 1) width <- rep_len(width, n_strings)[ido] - width[is.na(width)] <- -1 - if (length(tracking) != 1) tracking <- rep_len(tracking, n_strings)[ido] - if (length(indent) != 1) indent <- rep_len(indent, n_strings)[ido] - if (length(hanging) != 1) hanging <- rep_len(hanging, n_strings)[ido] - if (length(space_before) != 1) space_before <- rep_len(space_before, n_strings)[ido] - if (length(space_after) != 1) space_after <- rep_len(space_after, n_strings)[ido] + size <- rep_len(size, n_strings)[ido] + res <- rep_len(res, n_strings)[ido] + lineheight <- rep_len(lineheight, n_strings)[ido] + align <- match.arg(align, c('left', 'center', 'right', 'justified-left', 'justified-center', 'justified-right', 'distributed'), TRUE) + align <- match(align, c('left', 'center', 'right', 'justified-left', 'justified-center', 'justified-right', 'distributed')) + align <- rep_len(align, n_strings)[ido] + hjust <- rep_len(hjust, n_strings)[ido] + vjust <- rep_len(vjust, n_strings)[ido] + max_width <- rep_len(max_width, n_strings)[ido] + max_width[is.na(max_width)] <- -1 + tracking <- rep_len(tracking, n_strings)[ido] + indent <- rep_len(indent, n_strings)[ido] + hanging <- rep_len(hanging, n_strings)[ido] + space_before <- rep_len(space_before, n_strings)[ido] + space_after <- rep_len(space_after, n_strings)[ido] - width <- width * res + max_width <- max_width * res + tracking <- tracking * res indent <- indent * res hanging <- hanging * res + space_before <- space_before * res / 72 + space_after <- space_after * res / 72 + if (!all(file.exists(path))) stop("path must point to a valid file", call. = FALSE) shape <- get_string_shape_c( - strings, id, path, as.integer(index), as.numeric(size), as.numeric(res), - as.numeric(lineheight), as.integer(align) - 1L, as.numeric(hjust), - as.numeric(vjust), as.numeric(width), as.numeric(tracking), + strings, id, path, as.integer(index), features, as.numeric(size), + as.numeric(res), as.numeric(lineheight), as.integer(align) - 1L, + as.numeric(hjust), as.numeric(vjust), as.numeric(max_width), as.numeric(tracking), as.numeric(indent), as.numeric(hanging), as.numeric(space_before), as.numeric(space_after) ) if (nrow(shape$shape) == 0) return(shape) shape$metrics$string <- vapply(split(strings, id), paste, character(1), collapse = '') - shape$shape$string_id <- ido[shape$shape$string_id] + shape$metrics[-1] <- lapply(shape$metrics[-1], function(x) x * 72 / res[!duplicated(id)]) + + shape$shape$string_id <- ido[(cumsum(c(0, rle(id)$lengths)) + 1)[shape$shape$metric_id] + shape$shape$string_id - 1] shape$shape <- shape$shape[order(shape$shape$string_id), , drop = FALSE] - #shape$shape$glyph <- intToUtf8(shape$shape$glyph, multiple = TRUE) - shape$shape$x_offset <- shape$shape$x_offset * (72 / res) - shape$shape$y_offset <- shape$shape$y_offset * (72 / res) - shape$shape$x_midpoint <- shape$shape$x_midpoint * (72 / res) + shape$shape$x_offset <- shape$shape$x_offset * (72 / res[shape$shape$string_id]) + shape$shape$y_offset <- shape$shape$y_offset * (72 / res[shape$shape$string_id]) + shape$shape$advance <- shape$shape$advance * (72 / res[shape$shape$string_id]) + shape$shape$ascender <- shape$shape$ascender * (72 / res[shape$shape$string_id]) + shape$shape$descender <- shape$shape$descender * (72 / res[shape$shape$string_id]) shape } #' Calculate the width of a string, ignoring new-lines @@ -167,6 +184,7 @@ shape_text <- function(strings, id = NULL, family = '', italic = FALSE, #' provided `res` value to convert it into absolute values. #' #' @export +#' @importFrom systemfonts match_fonts #' #' @examples #' strings <- c('A short string', 'A very very looong string') @@ -177,18 +195,13 @@ text_width <- function(strings, family = '', italic = FALSE, bold = FALSE, index = 0) { n_strings <- length(strings) if (is.null(path)) { - if (all(c(length(family), length(italic), length(bold)) == 1)) { - loc <- systemfonts::match_font(family, italic, bold) - path <- loc$path - index <- loc$index - } else { - family <- rep_len(family, n_strings) - italic <- rep_len(italic, n_strings) - bold <- rep_len(bold, n_strings) - loc <- Map(systemfonts::match_font, family = family, italic = italic, bold = bold) - path <- vapply(loc, `[[`, character(1L), 1, USE.NAMES = FALSE) - index <- vapply(loc, `[[`, integer(1L), 2, USE.NAMES = FALSE) - } + fonts <- match_fonts( + rep_len(family, n_strings), + rep_len(italic, n_strings), + ifelse(rep_len(bold, n_strings), "bold", "normal") + ) + path <- fonts$path + index <- fonts$index } else { if (!all(c(length(path), length(index)) == 1)) { path <- rep_len(path, n_strings) diff --git a/R/textshaping-package.R b/R/textshaping-package.R index ab8a9e3..3720fc0 100644 --- a/R/textshaping-package.R +++ b/R/textshaping-package.R @@ -4,7 +4,8 @@ # The following block is used by usethis to automatically manage # roxygen namespace tags. Modify with care! ## usethis namespace: start -#' @useDynLib textshaping, .registration = TRUE +#' @importFrom lifecycle deprecated #' @importFrom systemfonts system_fonts +#' @useDynLib textshaping, .registration = TRUE ## usethis namespace: end NULL diff --git a/man/figures/lifecycle-archived.svg b/man/figures/lifecycle-archived.svg index 48f72a6..745ab0c 100644 --- a/man/figures/lifecycle-archived.svg +++ b/man/figures/lifecycle-archived.svg @@ -1 +1,21 @@ - lifecyclelifecyclearchivedarchived \ No newline at end of file + + lifecycle: archived + + + + + + + + + + + + + + + lifecycle + + archived + + diff --git a/man/figures/lifecycle-defunct.svg b/man/figures/lifecycle-defunct.svg index 01452e5..d5c9559 100644 --- a/man/figures/lifecycle-defunct.svg +++ b/man/figures/lifecycle-defunct.svg @@ -1 +1,21 @@ -lifecyclelifecycledefunctdefunct \ No newline at end of file + + lifecycle: defunct + + + + + + + + + + + + + + + lifecycle + + defunct + + diff --git a/man/figures/lifecycle-deprecated.svg b/man/figures/lifecycle-deprecated.svg index 4baaee0..b61c57c 100644 --- a/man/figures/lifecycle-deprecated.svg +++ b/man/figures/lifecycle-deprecated.svg @@ -1 +1,21 @@ -lifecyclelifecycledeprecateddeprecated \ No newline at end of file + + lifecycle: deprecated + + + + + + + + + + + + + + + lifecycle + + deprecated + + diff --git a/man/figures/lifecycle-experimental.svg b/man/figures/lifecycle-experimental.svg index d1d060e..5d88fc2 100644 --- a/man/figures/lifecycle-experimental.svg +++ b/man/figures/lifecycle-experimental.svg @@ -1 +1,21 @@ -lifecyclelifecycleexperimentalexperimental \ No newline at end of file + + lifecycle: experimental + + + + + + + + + + + + + + + lifecycle + + experimental + + diff --git a/man/figures/lifecycle-maturing.svg b/man/figures/lifecycle-maturing.svg index df71310..897370e 100644 --- a/man/figures/lifecycle-maturing.svg +++ b/man/figures/lifecycle-maturing.svg @@ -1 +1,21 @@ -lifecyclelifecyclematuringmaturing \ No newline at end of file + + lifecycle: maturing + + + + + + + + + + + + + + + lifecycle + + maturing + + diff --git a/man/figures/lifecycle-questioning.svg b/man/figures/lifecycle-questioning.svg index 08ee0c9..7c1721d 100644 --- a/man/figures/lifecycle-questioning.svg +++ b/man/figures/lifecycle-questioning.svg @@ -1 +1,21 @@ -lifecyclelifecyclequestioningquestioning \ No newline at end of file + + lifecycle: questioning + + + + + + + + + + + + + + + lifecycle + + questioning + + diff --git a/man/figures/lifecycle-soft-deprecated.svg b/man/figures/lifecycle-soft-deprecated.svg new file mode 100644 index 0000000..9c166ff --- /dev/null +++ b/man/figures/lifecycle-soft-deprecated.svg @@ -0,0 +1,21 @@ + + lifecycle: soft-deprecated + + + + + + + + + + + + + + + lifecycle + + soft-deprecated + + diff --git a/man/figures/lifecycle-stable.svg b/man/figures/lifecycle-stable.svg index e015dc8..9bf21e7 100644 --- a/man/figures/lifecycle-stable.svg +++ b/man/figures/lifecycle-stable.svg @@ -1 +1,29 @@ -lifecyclelifecyclestablestable \ No newline at end of file + + lifecycle: stable + + + + + + + + + + + + + + + + lifecycle + + + + stable + + + diff --git a/man/figures/lifecycle-superseded.svg b/man/figures/lifecycle-superseded.svg index 75f24f5..db8d757 100644 --- a/man/figures/lifecycle-superseded.svg +++ b/man/figures/lifecycle-superseded.svg @@ -1 +1,21 @@ - lifecyclelifecyclesupersededsuperseded \ No newline at end of file + + lifecycle: superseded + + + + + + + + + + + + + + + lifecycle + + superseded + + diff --git a/man/get_font_features.Rd b/man/get_font_features.Rd index 9b57b33..2898796 100644 --- a/man/get_font_features.Rd +++ b/man/get_font_features.Rd @@ -13,11 +13,11 @@ get_font_features( ) } \arguments{ -\item{family}{The name of the font family} +\item{family}{The name of the font families to match} -\item{italic}{logicals indicating the font style} +\item{italic}{logical indicating the font slant} -\item{bold}{logicals indicating the font style} +\item{bold}{logical indicating whether the font weight} \item{path, index}{path an index of a font file to circumvent lookup based on family and style} diff --git a/man/shape_text.Rd b/man/shape_text.Rd index 5449709..2e66906 100644 --- a/man/shape_text.Rd +++ b/man/shape_text.Rd @@ -9,21 +9,24 @@ shape_text( id = NULL, family = "", italic = FALSE, - bold = FALSE, + weight = "normal", + width = "normal", + features = font_feature(), size = 12, res = 72, lineheight = 1, align = "left", hjust = 0, vjust = 0, - width = NA, + max_width = NA, tracking = 0, indent = 0, hanging = 0, space_before = 0, space_after = 0, path = NULL, - index = 0 + index = 0, + bold = deprecated() ) } \arguments{ @@ -32,24 +35,38 @@ shape_text( \item{id}{A vector grouping the strings together. If strings share an id the shaping will continue between strings} -\item{family}{The name of the font family} +\item{family}{The name of the font families to match} -\item{italic}{logicals indicating the font style} +\item{italic}{logical indicating the font slant} -\item{bold}{logicals indicating the font style} +\item{weight}{The weight to query for, either in numbers (\code{0}, \code{100}, \code{200}, +\code{300}, \code{400}, \code{500}, \code{600}, \code{700}, \code{800}, or \code{900}) or strings (\code{"undefined"}, +\code{"thin"}, \code{"ultralight"}, \code{"light"}, \code{"normal"}, \code{"medium"}, \code{"semibold"}, +\code{"bold"}, \code{"ultrabold"}, or \code{"heavy"}). \code{NA} will be interpreted as +\code{"undefined"}/\code{0}} -\item{size}{The pointsize of the font to use for size related measures} +\item{width}{The width to query for either in numbers (\code{0}, \code{1}, \code{2}, +\code{3}, \code{4}, \code{5}, \code{6}, \code{7}, \code{8}, or \code{9}) or strings (\code{"undefined"}, +\code{"ultracondensed"}, \code{"extracondensed"}, \code{"condensed"}, \code{"semicondensed"}, +\code{"normal"}, \code{"semiexpanded"}, \code{"expanded"}, \code{"extraexpanded"}, or +\code{"ultraexpanded"}). \code{NA} will be interpreted as \code{"undefined"}/\code{0}} -\item{res}{The ppi of the size related mesures} +\item{features}{A \code{\link[systemfonts:font_feature]{systemfonts::font_feature()}} object or a list of them, +giving the OpenType font features to set} + +\item{size}{The size in points to use for the font} + +\item{res}{The resolution to use when doing the shaping. Should optimally +match the resolution used when rendering the glyphs.} \item{lineheight}{A multiplier for the lineheight} -\item{align}{Within text box alignment, either \code{'left'}, \code{'center'}, or -\code{'right'}} +\item{align}{Within text box alignment, either \code{'left'}, \code{'center'}, \code{'right'}, +\code{'justified-left'}, \code{'justified-right'}, \code{'justified-center'}, or \code{'distributed'}} \item{hjust, vjust}{The justification of the textbox surrounding the text} -\item{width}{The requested with of the string in inches. Setting this to +\item{max_width}{The requested with of the string in inches. Setting this to something other than \code{NA} will turn on word wrapping.} \item{tracking}{Tracking of the glyphs (space adjustment) measured in 1/1000 @@ -65,6 +82,8 @@ measured in points} \item{path, index}{path an index of a font file to circumvent lookup based on family and style} + +\item{bold}{logical indicating whether the font weight} } \value{ A list with two element: \code{shape} contains the position of each glyph, diff --git a/man/text_width.Rd b/man/text_width.Rd index 8f4bfd9..6e7b042 100644 --- a/man/text_width.Rd +++ b/man/text_width.Rd @@ -19,11 +19,11 @@ text_width( \arguments{ \item{strings}{A character vector of strings} -\item{family}{The name of the font family} +\item{family}{The name of the font families to match} -\item{italic}{logicals indicating the font style} +\item{italic}{logical indicating the font slant} -\item{bold}{logicals indicating the font style} +\item{bold}{logical indicating whether the font weight} \item{size}{The pointsize of the font to use for size related measures} diff --git a/src/cpp11.cpp b/src/cpp11.cpp index e69c9e0..6d22ea2 100644 --- a/src/cpp11.cpp +++ b/src/cpp11.cpp @@ -13,10 +13,10 @@ extern "C" SEXP _textshaping_get_face_features_c(SEXP path, SEXP index) { END_CPP11 } // string_metrics.h -list get_string_shape_c(strings string, integers id, strings path, integers index, doubles size, doubles res, doubles lineheight, integers align, doubles hjust, doubles vjust, doubles width, doubles tracking, doubles indent, doubles hanging, doubles space_before, doubles space_after); -extern "C" SEXP _textshaping_get_string_shape_c(SEXP string, SEXP id, SEXP path, SEXP index, SEXP size, SEXP res, SEXP lineheight, SEXP align, SEXP hjust, SEXP vjust, SEXP width, SEXP tracking, SEXP indent, SEXP hanging, SEXP space_before, SEXP space_after) { +list get_string_shape_c(strings string, integers id, strings path, integers index, list_of features, doubles size, doubles res, doubles lineheight, integers align, doubles hjust, doubles vjust, doubles width, doubles tracking, doubles indent, doubles hanging, doubles space_before, doubles space_after); +extern "C" SEXP _textshaping_get_string_shape_c(SEXP string, SEXP id, SEXP path, SEXP index, SEXP features, SEXP size, SEXP res, SEXP lineheight, SEXP align, SEXP hjust, SEXP vjust, SEXP width, SEXP tracking, SEXP indent, SEXP hanging, SEXP space_before, SEXP space_after) { BEGIN_CPP11 - return cpp11::as_sexp(get_string_shape_c(cpp11::as_cpp>(string), cpp11::as_cpp>(id), cpp11::as_cpp>(path), cpp11::as_cpp>(index), cpp11::as_cpp>(size), cpp11::as_cpp>(res), cpp11::as_cpp>(lineheight), cpp11::as_cpp>(align), cpp11::as_cpp>(hjust), cpp11::as_cpp>(vjust), cpp11::as_cpp>(width), cpp11::as_cpp>(tracking), cpp11::as_cpp>(indent), cpp11::as_cpp>(hanging), cpp11::as_cpp>(space_before), cpp11::as_cpp>(space_after))); + return cpp11::as_sexp(get_string_shape_c(cpp11::as_cpp>(string), cpp11::as_cpp>(id), cpp11::as_cpp>(path), cpp11::as_cpp>(index), cpp11::as_cpp>>(features), cpp11::as_cpp>(size), cpp11::as_cpp>(res), cpp11::as_cpp>(lineheight), cpp11::as_cpp>(align), cpp11::as_cpp>(hjust), cpp11::as_cpp>(vjust), cpp11::as_cpp>(width), cpp11::as_cpp>(tracking), cpp11::as_cpp>(indent), cpp11::as_cpp>(hanging), cpp11::as_cpp>(space_before), cpp11::as_cpp>(space_after))); END_CPP11 } // string_metrics.h @@ -31,7 +31,7 @@ extern "C" { static const R_CallMethodDef CallEntries[] = { {"_textshaping_get_face_features_c", (DL_FUNC) &_textshaping_get_face_features_c, 2}, {"_textshaping_get_line_width_c", (DL_FUNC) &_textshaping_get_line_width_c, 6}, - {"_textshaping_get_string_shape_c", (DL_FUNC) &_textshaping_get_string_shape_c, 16}, + {"_textshaping_get_string_shape_c", (DL_FUNC) &_textshaping_get_string_shape_c, 17}, {NULL, NULL, 0} }; } diff --git a/src/string_metrics.cpp b/src/string_metrics.cpp index e2c7432..18ed6d2 100644 --- a/src/string_metrics.cpp +++ b/src/string_metrics.cpp @@ -16,8 +16,9 @@ using namespace cpp11; #ifdef NO_HARFBUZZ_FRIBIDI list get_string_shape_c(strings string, integers id, strings path, integers index, - doubles size, doubles res, doubles lineheight, integers align, - doubles hjust, doubles vjust, doubles width, doubles tracking, + list_of features, doubles size, doubles res, + doubles lineheight, integers align, doubles hjust, + doubles vjust, doubles width, doubles tracking, doubles indent, doubles hanging, doubles space_before, doubles space_after) { Rprintf("textshaping has been compiled without HarfBuzz and/or Fribidi. Please install system dependencies and recompile\n"); @@ -90,43 +91,53 @@ int ts_string_shape_old(const char* string, FontSettings font_info, double size, #else +std::vector< std::vector > create_font_features(list_of features) { + std::vector< std::vector > res; + + for (R_xlen_t i = 0; i < features.size(); ++i) { + res.emplace_back(); + strings tags = as_cpp(features[i][0]); + integers vals = as_cpp(features[i][1]); + for (R_xlen_t j = 0; j < tags.size(); ++j) { + const char* f = Rf_translateCharUTF8(tags[j]); + res.back().push_back({{f[0], f[1], f[2], f[3]}, vals[j]}); + } + } + + return res; +} + +std::vector create_font_settings(strings path, integers index, std::vector< std::vector >& features) { + std::vector res; + + for (R_xlen_t i = 0; i < path.size(); ++i) { + res.emplace_back(); + strncpy(res.back().file, Rf_translateCharUTF8(path[i]), PATH_MAX); + res.back().file[PATH_MAX] = '\0'; + res.back().index = index[i]; + res.back().features = features[i].data(); + res.back().n_features = features[i].size(); + } + + return res; +} + list get_string_shape_c(strings string, integers id, strings path, integers index, - doubles size, doubles res, doubles lineheight, integers align, - doubles hjust, doubles vjust, doubles width, doubles tracking, + list_of features, doubles size, doubles res, + doubles lineheight, integers align, doubles hjust, + doubles vjust, doubles width, doubles tracking, doubles indent, doubles hanging, doubles space_before, doubles space_after) { int n_strings = string.size(); - bool one_path = path.size() == 1; - const char* first_path = Rf_translateCharUTF8(path[0]); - int first_index = index[0]; - bool one_size = size.size() == 1; - double first_size = size[0]; - bool one_res = res.size() == 1; - double first_res = res[0]; - bool one_lht = lineheight.size() == 1; - double first_lht = lineheight[0]; - bool one_align = align.size() == 1; - int first_align = align[0]; - bool one_hjust = hjust.size() == 1; - double first_hjust = hjust[0]; - bool one_vjust = vjust.size() == 1; - double first_vjust = vjust[0]; - bool one_width = width.size() == 1; - double first_width = width[0] * 64; - bool one_tracking = tracking.size() == 1; - double first_tracking = tracking[0]; - bool one_indent = indent.size() == 1; - double first_indent = indent[0] * 64; - bool one_hanging = hanging.size() == 1; - double first_hanging = hanging[0] * 64; - bool one_before = space_before.size() == 1; - double first_before = space_before[0] * 64; - bool one_after = space_after.size() == 1; - double first_after = space_after[0] * 64; + auto all_features = create_font_features(features); + auto fonts = create_font_settings(path, index, all_features); // Return Columns - writable::integers glyph, glyph_id, metric_id, string_id; - writable::doubles x_offset, y_offset, x_midpoint, widths, heights, left_bearings, right_bearings, top_bearings, bottom_bearings, left_border, top_border, pen_x, pen_y; + writable::integers glyph, glyph_id, metric_id, string_id, fontindex; + writable::doubles x_offset, y_offset, widths, heights, left_bearings, right_bearings, + top_bearings, bottom_bearings, left_border, top_border, pen_x, pen_y, fontsize, + advance, ascender, descender; + writable::strings fontpath; // Shape the text int cur_id = id[0] - 1; // make sure it differs from first @@ -137,31 +148,19 @@ list get_string_shape_c(strings string, integers id, strings path, integers inde const char* this_string = Rf_translateCharUTF8(string[i]); int this_id = id[i]; if (cur_id == this_id) { - success = shaper.add_string(this_string, - one_path ? first_path : Rf_translateCharUTF8(path[i]), - one_path ? first_index : index[i], - one_size ? first_size : size[i], - one_tracking ? first_tracking : tracking[i]); + success = shaper.add_string(this_string, fonts[i], size[i], tracking[i], cpp11::is_na(string[i])); + if (!success) { Rf_error("Failed to shape string (%s) with font file (%s) with freetype error %i", this_string, Rf_translateCharUTF8(path[i]), shaper.error_code); } } else { cur_id = this_id; - success = shaper.shape_string(this_string, - one_path ? first_path : Rf_translateCharUTF8(path[i]), - one_path ? first_index : index[i], - one_size ? first_size : size[i], - one_res ? first_res : res[i], - one_lht ? first_lht : lineheight[i], - one_align ? first_align : align[i], - one_hjust ? first_hjust : hjust[i], - one_vjust ? first_vjust : vjust[i], - one_width ? first_width : width[i] * 64, - one_tracking ? first_tracking : tracking[i], - one_indent ? first_indent : indent[i] * 64, - one_hanging ? first_hanging : hanging[i] * 64, - one_before ? first_before : space_before[i] * 64, - one_after ? first_after : space_after[i] * 64); + success = shaper.shape_string(this_string, fonts[i], size[i], res[i], + lineheight[i], align[i], hjust[i], vjust[i], + width[i] * 64.0, tracking[i], indent[i] * 64.0, + hanging[i] * 64.0, space_before[i] * 64.0, + space_after[i] * 64.0, cpp11::is_na(string[i])); + if (!success) { Rf_error("Failed to shape string (%s) with font file (%s) with freetype error %i", this_string, Rf_translateCharUTF8(STRING_ELT(path, i)), shaper.error_code); } @@ -174,13 +173,19 @@ list get_string_shape_c(strings string, integers id, strings path, integers inde } int n_glyphs = shaper.glyph_id.size(); for (int j = 0; j < n_glyphs; j++) { - glyph.push_back((int) shaper.glyph_cluster[j]); + if (shaper.must_break[j]) continue; // Don't add any linebreak chars as they often map to null glyph + glyph.push_back((int) shaper.glyph_cluster[j] + 1); glyph_id.push_back((int) shaper.glyph_id[j]); metric_id.push_back(pen_x.size() + 1); string_id.push_back(shaper.string_id[j] + 1); x_offset.push_back(double(shaper.x_pos[j]) / 64.0); y_offset.push_back(double(shaper.y_pos[j]) / 64.0); - x_midpoint.push_back(double(shaper.x_mid[j]) / 64.0); + fontpath.push_back(shaper.fontfile[j]); + fontindex.push_back((int) shaper.fontindex[j]); + fontsize.push_back(shaper.fontsize[j]); + advance.push_back(double(shaper.advance[j]) / 64.0); + ascender.push_back(double(shaper.ascender[j]) / 64.0); + descender.push_back(double(shaper.descender[j]) / 64.0); } widths.push_back(double(shaper.width) / 64.0); heights.push_back(double(shaper.height) / 64.0); @@ -197,28 +202,33 @@ list get_string_shape_c(strings string, integers id, strings path, integers inde writable::strings str(pen_x.size()); writable::data_frame string_df({ - "string"_nm = (SEXP) str, - "width"_nm = (SEXP) widths, - "height"_nm = (SEXP) heights, - "left_bearing"_nm = (SEXP) left_bearings, - "right_bearing"_nm = (SEXP) right_bearings, - "top_bearing"_nm = (SEXP) top_bearings, - "bottom_bearing"_nm = (SEXP) bottom_bearings, - "left_border"_nm = (SEXP) left_border, - "top_border"_nm = (SEXP) top_border, - "pen_x"_nm = (SEXP) pen_x, - "pen_y"_nm = (SEXP) pen_y + "string"_nm = str, + "width"_nm = widths, + "height"_nm = heights, + "left_bearing"_nm = left_bearings, + "right_bearing"_nm = right_bearings, + "top_bearing"_nm = top_bearings, + "bottom_bearing"_nm = bottom_bearings, + "left_border"_nm = left_border, + "top_border"_nm = top_border, + "pen_x"_nm = pen_x, + "pen_y"_nm = pen_y }); string_df.attr("class") = writable::strings({"tbl_df", "tbl", "data.frame"}); writable::data_frame info_df({ - "glyph"_nm = (SEXP) glyph, - "index"_nm = (SEXP) glyph_id, - "metric_id"_nm = (SEXP) metric_id, - "string_id"_nm = (SEXP) string_id, - "x_offset"_nm = (SEXP) x_offset, - "y_offset"_nm = (SEXP) y_offset, - "x_midpoint"_nm = (SEXP) x_midpoint + "glyph"_nm = glyph, + "index"_nm = glyph_id, + "metric_id"_nm = metric_id, + "string_id"_nm = string_id, + "x_offset"_nm = x_offset, + "y_offset"_nm = y_offset, + "font_path"_nm = fontpath, + "font_index"_nm = fontindex, + "font_size"_nm = fontsize, + "advance"_nm = advance, + "ascender"_nm = ascender, + "descender"_nm = descender }); info_df.attr("class") = writable::strings({"tbl_df", "tbl", "data.frame"}); @@ -268,18 +278,23 @@ int ts_string_width(const char* string, FontSettings font_info, double size, double res, int include_bearing, double* width) { BEGIN_CPP11 HarfBuzzShaper& shaper = get_hb_shaper(); - bool success = shaper.single_line_shape( - string, font_info, size, res + shaper.error_code = 0; + const ShapeInfo string_shape = shaper.shape_text_run( + string, font_info, size, res, 0 ); - if (!success) { + if (shaper.error_code != 0) { return shaper.error_code; } - int32_t width_tmp = shaper.last_shape_info.width; + int32_t width_tmp = 0; + for (size_t i = 0; i < string_shape.glyph_id.size(); ++i) { + width_tmp += string_shape.x_advance[i]; + } + if (!include_bearing) { - width_tmp -= shaper.last_shape_info.left_bearing; - width_tmp -= shaper.last_shape_info.right_bearing; + width_tmp -= string_shape.x_bear[0]; + width_tmp -= string_shape.x_advance.back() - string_shape.x_bear.back() - string_shape.width.back(); } *width = double(width_tmp) / 64.0; @@ -294,31 +309,35 @@ int ts_string_shape(const char* string, FontSettings font_info, double size, std::vector& fallback_scaling) { BEGIN_CPP11 HarfBuzzShaper& shaper = get_hb_shaper(); - bool success = shaper.single_line_shape( - string, font_info, size, res + shaper.error_code = 0; + const ShapeInfo string_shape = shaper.shape_text_run( + string, font_info, size, res, 0 ); - if (!success) { + + if (shaper.error_code != 0) { return shaper.error_code; } - int n_glyphs = shaper.last_shape_info.x_pos.size(); + + size_t n_glyphs = string_shape.glyph_id.size(); loc.clear(); - if (n_glyphs == 0) { - id.clear(); - font.clear(); - fallbacks.clear(); - fallback_scaling.clear(); - } else { - for (int i = 0; i < n_glyphs; ++i) { - loc.push_back({ - double(shaper.last_shape_info.x_pos[i]) / 64.0, - 0.0 - }); - } - id.assign(shaper.last_shape_info.glyph_id.begin(), shaper.last_shape_info.glyph_id.end()); - font.assign(shaper.last_shape_info.font.begin(), shaper.last_shape_info.font.end()); - fallbacks.assign(shaper.last_shape_info.fallbacks.begin(), shaper.last_shape_info.fallbacks.end()); - fallback_scaling.assign(shaper.last_shape_info.fallback_scaling.begin(), shaper.last_shape_info.fallback_scaling.end()); + id.clear(); + font.clear(); + fallbacks.clear(); + fallback_scaling.clear(); + int32_t x = 0; + int32_t y = 0; + for (size_t i = 0; i < n_glyphs; ++i) { + loc.push_back({ + double(x + string_shape.x_offset[i]) / 64.0, + double(y + string_shape.y_offset[i]) / 64.0 + }); + x += string_shape.x_advance[i]; + y += string_shape.y_advance[i]; } + id.assign(string_shape.glyph_id.begin(), string_shape.glyph_id.end()); + font.assign(string_shape.font.begin(), string_shape.font.end()); + fallbacks.assign(string_shape.fallbacks.begin(), string_shape.fallbacks.end()); + fallback_scaling.assign(string_shape.fallback_scaling.begin(), string_shape.fallback_scaling.end()); END_CPP11_NO_RETURN return 0; diff --git a/src/string_metrics.h b/src/string_metrics.h index 6b42dd3..2f77fd7 100644 --- a/src/string_metrics.h +++ b/src/string_metrics.h @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -22,8 +23,9 @@ struct Point { [[cpp11::register]] list get_string_shape_c(strings string, integers id, strings path, integers index, - doubles size, doubles res, doubles lineheight, integers align, - doubles hjust, doubles vjust, doubles width, doubles tracking, + list_of features, doubles size, doubles res, + doubles lineheight, integers align, doubles hjust, + doubles vjust, doubles width, doubles tracking, doubles indent, doubles hanging, doubles space_before, doubles space_after); diff --git a/src/string_shape.cpp b/src/string_shape.cpp index 52071c3..3c37653 100644 --- a/src/string_shape.cpp +++ b/src/string_shape.cpp @@ -10,63 +10,19 @@ UTF_UCS HarfBuzzShaper::utf_converter = UTF_UCS(); LRU_Cache > HarfBuzzShaper::bidi_cache = {1000}; LRU_Cache HarfBuzzShaper::shape_cache = {1000}; -std::vector HarfBuzzShaper::glyph_id = {}; -std::vector HarfBuzzShaper::glyph_cluster = {}; -std::vector HarfBuzzShaper::string_id = {}; -std::vector HarfBuzzShaper::x_pos = {}; -std::vector HarfBuzzShaper::y_pos = {}; -std::vector HarfBuzzShaper::x_mid = {}; -std::vector HarfBuzzShaper::x_advance = {}; -std::vector HarfBuzzShaper::x_offset = {}; -std::vector HarfBuzzShaper::left_bear = {}; -std::vector HarfBuzzShaper::right_bear = {}; -std::vector HarfBuzzShaper::top_extend = {}; -std::vector HarfBuzzShaper::bottom_extend = {}; -std::vector HarfBuzzShaper::ascenders = {}; -std::vector HarfBuzzShaper::descenders = {}; -std::vector HarfBuzzShaper::may_break = {}; -std::vector HarfBuzzShaper::must_break = {}; -std::vector HarfBuzzShaper::may_stretch = {}; -ShapeID HarfBuzzShaper::last_shape_id = {}; -ShapeID HarfBuzzShaper::temp_shape_id = {}; -ShapeInfo HarfBuzzShaper::last_shape_info = {}; - -bool HarfBuzzShaper::shape_string(const char* string, const char* fontfile, - int index, double size, double res, double lineheight, + +bool HarfBuzzShaper::shape_string(const char* string, FontSettings& font_info, + double size, double res, double lineheight, int align, double hjust, double vjust, double width, double tracking, double ind, double hang, double before, - double after) { + double after, bool spacer) { reset(); - int error = 0; - FT_Face face = get_cached_face(fontfile, index, size, res, &error); - if (error != 0) { - error_code = error; - return false; - } - hb_font_t *font = hb_ft_font_create(face, NULL); - - int n_chars = 0; - const uint32_t* utc_string = utf_converter.convert_to_ucs(string, n_chars); - - std::vector embeddings = {}; - - if (n_chars > 1) { - std::string key(string); - if (!bidi_cache.get(key, embeddings)) { - embeddings = get_bidi_embeddings(utc_string, n_chars); - bidi_cache.add(key, embeddings); - } - } else { - embeddings.push_back(0); - } - - max_width = width; + max_width = width + 1; // To prevent rounding errors indent = ind; hanging = hang; space_before = before; space_after = after; - cur_tracking = tracking; cur_res = res; cur_lineheight = lineheight; @@ -74,255 +30,276 @@ bool HarfBuzzShaper::shape_string(const char* string, const char* fontfile, cur_hjust = hjust; cur_vjust = vjust; - int start = 0; - for (size_t i = 0; i < embeddings.size(); ++i) { - if (i == embeddings.size() - 1 || embeddings[i] != embeddings[i + 1]) { - hb_buffer_reset(buffer); - hb_buffer_add_utf32(buffer, utc_string, n_chars, start, i - start + 1); - hb_buffer_guess_segment_properties(buffer); - - bool success = shape_glyphs(font, utc_string + start, i - start + 1); - if (!success) { - return false; - } - - start = i + 1; - } - } - - hb_font_destroy(font); - //FT_Done_Face(face); - return true; + return add_string(string, font_info, size, tracking, spacer); } -bool HarfBuzzShaper::add_string(const char* string, const char* fontfile, - int index, double size, double tracking) { - cur_string++; - int error = 0; - FT_Face face = get_cached_face(fontfile, index, size, cur_res, &error); - if (error != 0) { - error_code = error; - return false; - } - hb_font_t *font = hb_ft_font_create(face, NULL); - - int n_chars = 0; - const uint32_t* utc_string = utf_converter.convert_to_ucs(string, n_chars); - - std::vector embeddings = {}; - - if (n_chars > 1) { - std::string key(string); - if (!bidi_cache.get(key, embeddings)) { - embeddings = get_bidi_embeddings(utc_string, n_chars); - bidi_cache.add(key, embeddings); - } - } else { - embeddings.push_back(0); +bool HarfBuzzShaper::add_string(const char* string, FontSettings& font_info, + double size, double tracking, bool spacer) { + if (spacer) { + return add_spacer(size, tracking); } - cur_tracking = tracking; - - int start = 0; - for (size_t i = 0; i < embeddings.size(); ++i) { - if (i == embeddings.size() - 1 || embeddings[i] != embeddings[i + 1]) { - hb_buffer_reset(buffer); - hb_buffer_add_utf32(buffer, utc_string, n_chars, start, i - start + 1); - hb_buffer_guess_segment_properties(buffer); - - bool success = shape_glyphs(font, utc_string + start, i - start + 1); - if (!success) { - return false; - } + error_code = 0; + shape_infos.push_back(shape_text_run(string, font_info, size, cur_res, tracking)); - start = i + 1; - } + if (error_code != 0) { + shape_infos.pop_back(); + return false; } - - hb_font_destroy(font); //FT_Done_Face(face); return true; } +bool HarfBuzzShaper::add_spacer(double height, double width) { + width *= 64.0 / 72.0; + double space_height = height * 64.0 * cur_res / 72.0; + FontSettings dummy_font = {"", 0, NULL, 0}; + ShapeInfo info = { + {0}, // glyph_id + {0}, // glyph_cluster + {int32_t(width)}, // x_advance + {0}, // y_advance + {0}, // x_offset + {0}, // y_offset + {0}, // x_bear + {int32_t(space_height)}, // y_bear + {int32_t(width)}, // width + {int32_t(space_height)}, // height + {int32_t(space_height)}, // ascenders + {0}, // descenders + {false}, // may_break + {false}, // must_break + {false}, // may_stretch + {0}, // font + {dummy_font}, // fallbacks + {height}, // fallback_size + {-1}, // fallback_scaling + true // ltr + }; + shape_infos.push_back(info); + return true; +} bool HarfBuzzShaper::finish_string() { - if (glyph_id.empty()) { + if (shape_infos.empty()) { return true; } + bool vertical = false; + + if (vertical) { + pen_y = indent; + pen_x = 0; + } else { + pen_x = indent; + pen_y = 0; + } + bool first_char = true; bool first_line = true; - pen_x += indent; - int last_space = -1; - int32_t last_nonspace_width = 0; - int32_t last_nonspace_bear = 0; int cur_line = 0; - double line_height = 0; - size_t glyph_counter = 0; int32_t max_descend = 0; int32_t max_ascend = 0; int32_t max_top_extend = 0; int32_t max_bottom_extend = 0; int32_t last_max_descend = 0; - bool no_break_last = true; - - for (unsigned int i = 0; i < glyph_id.size(); ++i) { - bool linebreak = must_break[i]; - bool might_break = may_break[i]; - bool last = i == glyph_id.size() - 1; - - bool soft_wrap = false; - - if (might_break || linebreak) { - last_space = i; - if (no_break_last) { - last_nonspace_width = pen_x; - last_nonspace_bear = i == 0 ? 0 : right_bear[i - 1]; + int breaktype = 0; + int32_t cur_linewidth = 0; + int32_t cur_left_bear = 0; + int32_t cur_right_bear = 0; + size_t last_line_start = 0; + + for (size_t j = 0; j < shape_infos.size(); ++j) { + ShapeInfo& cur_shape = shape_infos[j]; + bool last_shape = j == shape_infos.size() - 1; + size_t start = cur_shape.ltr ? 0 : cur_shape.glyph_id.size(); + + while (true) { + size_t end; + bool last; + if (cur_shape.glyph_id.size() == 0) { + end = 0; + last = last_shape; + max_ascend = std::max(max_ascend, cur_shape.ascenders[0]); + max_descend = std::min(max_descend, cur_shape.descenders[0]); + } else { + end = fill_out_width(start, max_width - pen_x, j, breaktype); + last = last_shape && end == (cur_shape.ltr ? cur_shape.glyph_id.size() : 0); + for (size_t i = std::min(start, end); i < std::max(start, end); ++i) { + glyph_id.push_back(cur_shape.glyph_id[i]); + glyph_cluster.push_back(cur_shape.glyph_cluster[i]); + fontfile.push_back(cur_shape.fallbacks[cur_shape.font[i]].file); + fontindex.push_back(cur_shape.fallbacks[cur_shape.font[i]].index); + fontsize.push_back(cur_shape.fallback_size[cur_shape.font[i]]); + advance.push_back(cur_shape.x_advance[i]); + ascender.push_back(cur_shape.ascenders[i]); + descender.push_back(cur_shape.descenders[i]); + + string_id.push_back(j); + line_id.push_back(cur_line); + x_pos.push_back(pen_x + cur_shape.x_offset[i]); + y_pos.push_back(pen_y + cur_shape.y_offset[i]); + + must_break.push_back(cur_shape.must_break[i]); + may_stretch.push_back(cur_shape.may_stretch[i]); + + if (vertical) { + pen_y += cur_shape.y_advance[i]; + if (first_char) cur_left_bear = cur_shape.y_bear[i]; + if (!cur_shape.may_break[i] || (cur_shape.ltr && first_char)) { + cur_linewidth += pen_y; + cur_right_bear = cur_shape.y_bear[i] + cur_shape.height[i]; + } + + // TODO: Awaiting knowledge on glyph metrics returned with vertical layouts + max_ascend = std::max(max_ascend, cur_shape.ascenders[i]); + max_top_extend = std::max(max_top_extend, cur_shape.y_bear[i]); + max_descend = std::max(max_descend, cur_shape.descenders[i]); + max_bottom_extend = std::max(max_bottom_extend, cur_shape.height[i] + cur_shape.y_bear[i]); + + + } else { + pen_x += cur_shape.x_advance[i]; + if (first_char) cur_left_bear = cur_shape.x_bear[i]; + if ((!cur_shape.may_break[i] && !cur_shape.must_break[i]) || (cur_shape.ltr && first_char)) { + cur_linewidth = pen_x; + cur_right_bear = cur_shape.x_advance[i] - cur_shape.x_bear[i] - cur_shape.width[i]; + } + + max_ascend = std::max(max_ascend, cur_shape.ascenders[i]); + max_top_extend = std::max(max_top_extend, cur_shape.y_bear[i]); + max_descend = std::min(max_descend, cur_shape.descenders[i]); + max_bottom_extend = std::min(max_bottom_extend, cur_shape.height[i] + cur_shape.y_bear[i]); + } + + if (!cur_shape.may_break[i]) first_char = false; + } } - } - no_break_last = !might_break; - - // Calculate top and bottom extend and ascender/descender + if (breaktype != 0 || last) { + line_width.push_back(cur_linewidth); + line_left_bear.push_back(cur_left_bear); + line_right_bear.push_back(cur_right_bear); - // Soft wrapping? - if (max_width > 0 && !first_char && pen_x + x_advance[i] > max_width && !might_break && !linebreak) { - // Rewind to last breaking char and set the soft_wrap flag - i = last_space >= 0 ? last_space : i - 1; - x_pos.resize(i + 1); - x_mid.resize(i + 1); - line_id.resize(i + 1); - soft_wrap = true; - last = false; - } else { - // No soft wrap, record pen position - x_pos.push_back(pen_x + x_offset[i]); - x_mid.push_back(x_advance[i] / 2); - line_id.push_back(cur_line); - } - - // If last char update terminal line info - if (last && !linebreak) { - last_nonspace_width = pen_x + x_advance[i]; - last_nonspace_bear = right_bear[i]; - } - if (first_char) { - line_left_bear.push_back(left_bear[i]); - pen_y -= space_before; - } - - // Handle new lines - if (linebreak || soft_wrap || last) { - // Record and reset line dim info - line_right_bear.push_back(last_nonspace_bear); - line_width.push_back(last_nonspace_width); - last_nonspace_bear = 0; - last_nonspace_width = 0; - last_space = -1; - no_break_last = true; - - // Calculate line dimensions - for (size_t j = glyph_counter; j < x_pos.size(); ++j) { - if (max_ascend < ascenders[j]) { - max_ascend = ascenders[j]; - } - if (max_top_extend < top_extend[j]) { - max_top_extend = top_extend[j]; + if (first_line) { + top_border = max_ascend + space_before; + top_bearing = top_border - max_top_extend; } - if (max_descend > descenders[j]) { - max_descend = descenders[j]; - } - if (max_bottom_extend > bottom_extend[j]) { - max_bottom_extend = bottom_extend[j]; + + double line_height = first_line ? 0 : (max_ascend - last_max_descend) * cur_lineheight; + for (size_t k = last_line_start; k < y_pos.size(); ++k) { + if (vertical) { + x_pos[k] -= line_height; + } else { + y_pos[k] -= line_height; + } } - } + if (vertical) { + pen_x -= line_height + (breaktype == 2 ? space_after + (last ? 0 : space_before) : 0); + pen_y = last ? (breaktype != 0 ? 0 : pen_y) : (breaktype == 1 ? hanging : indent); - // Move pen based on indent and line height - line_height = (max_ascend - last_max_descend) * cur_lineheight; - if (last) { - pen_x = (linebreak || soft_wrap) ? 0 : pen_x + x_advance[i]; - } else { - pen_x = soft_wrap ? hanging : indent; - } - pen_y -= first_line ? 0 : line_height; - bottom -= line_height; - // Fill up y_pos based on calculated pen position - for (; glyph_counter < x_pos.size(); ++glyph_counter) { - y_pos.push_back(pen_y); - } - // Move pen_y further down based on paragraph spacing - // TODO: Add per string paragraph spacing - if (linebreak) { - pen_y -= space_after; - if (last) { - pen_y -= line_height; - bottom -= line_height; + } else { + pen_y -= line_height + (breaktype == 2 ? space_after + (last ? 0 : space_before) : 0); + pen_x = last ? (breaktype != 0 ? 0 : pen_x) : (breaktype == 1 ? hanging : indent); } - } - if (first_line) { - top_border = max_ascend; - top_bearing = top_border - max_top_extend; - } - // Reset flags and counters - last_max_descend = max_descend; - if (!last) { - max_ascend = 0; - max_descend = 0; - max_top_extend = 0; - max_bottom_extend = 0; + + last_max_descend = max_descend; + cur_linewidth = 0; + cur_left_bear = 0; + cur_right_bear = 0; first_line = false; cur_line++; first_char = true; + if (!last) { // We need this for calculating box dimensions + max_ascend = 0; + max_descend = 0; + max_top_extend = 0; + max_bottom_extend = 0; + } else if (breaktype == 2) { + pen_y -= (max_ascend - last_max_descend) * cur_lineheight + space_before; + max_bottom_extend = 0; + } } - } else { - // No line break - advance the pen - pen_x += x_advance[i]; - first_char = false; + if (breaktype != 0) { + last_line_start = y_pos.size(); + } + if (end == 0 || end == cur_shape.glyph_id.size()) { + break; + } + start = end; } } - height = top_border - pen_y - max_descend + space_after; - bottom_bearing = max_bottom_extend - max_descend; + + int32_t bottom = pen_y + max_descend - space_after; + height = top_border - bottom; + bottom_bearing = max_bottom_extend - max_descend + space_after; int max_width_ind = std::max_element(line_width.begin(), line_width.end()) - line_width.begin(); width = max_width < 0 ? line_width[max_width_ind] : max_width; if (cur_align == 1 || cur_align == 2) { - for (unsigned int i = 0; i < x_pos.size(); ++i) { + for (size_t i = 0; i < x_pos.size(); ++i) { int index = line_id[i]; int32_t lwd = line_width[index]; x_pos[i] = cur_align == 1 ? x_pos[i] + width/2 - lwd/2 : x_pos[i] + width - lwd; } + pen_x = cur_align == 1 ? pen_x + width/2 - line_width.back()/2 : pen_x + width - line_width.back(); } - if (cur_align == 3) { + if (cur_align == 3 || cur_align == 4 || cur_align == 5) { std::vector n_stretches(line_width.size(), 0); std::vector no_stretch(line_width.size(), false); - for (unsigned int i = 0; i < x_pos.size(); ++i) { + for (size_t i = 0; i < x_pos.size(); ++i) { int index = line_id[i]; - no_stretch[index] = no_stretch[index] || must_break[i]; + no_stretch[index] = no_stretch[index] || index == line_width.size() - 1 || must_break[i]; if (may_stretch[i] && i-1 < x_pos.size() && index == line_id[i+1]) { n_stretches[index]++; } } int32_t cum_move = 0; - for (unsigned int i = 0; i < x_pos.size(); ++i) { + for (size_t i = 0; i < x_pos.size(); ++i) { int index = line_id[i]; - if (no_stretch[index]) continue; + int32_t lwd = line_width[index]; + if (no_stretch[index]) { + if (cur_align == 4) { + x_pos[i] = x_pos[i] + width/2 - lwd/2; + } else if (cur_align == 5) { + x_pos[i] = x_pos[i] + width - lwd; + } + continue; + } if (i == 0 || line_id[i-1] != index) { cum_move = 0; } x_pos[i] += cum_move; + if (n_stretches[index] == 0) { + if (cur_align == 4) { + x_pos[i] = x_pos[i] + width/2 - lwd/2; + } else if (cur_align == 5) { + x_pos[i] = x_pos[i] + width - lwd; + } + } if (may_stretch[i]) { cum_move += (width - line_width[index]) / n_stretches[index]; } } + pen_x += cum_move; + if (no_stretch.back() || n_stretches.back() == 0) { + if (cur_align == 4) { + pen_x += width/2 - line_width.back()/2; + } else if (cur_align == 5) { + pen_x += width - line_width.back(); + } + } + for (size_t i = 0; i < line_width.size(); ++i) { + if (!no_stretch[i] && n_stretches[i] != 0) line_width[i] = width; + } } - if (cur_align == 4) { + if (cur_align == 6) { std::vector n_glyphs(line_width.size(), 0); - for (unsigned int i = 0; i < x_pos.size(); ++i) { + for (size_t i = 0; i < x_pos.size(); ++i) { int index = line_id[i]; - if (!must_break[i] && i-1 < x_pos.size() && index == line_id[i+1]) { + if (!must_break[i] && (i == x_pos.size()-1 || index == line_id[i+1])) { n_glyphs[index]++; } - if (i == x_pos.size()-1) n_glyphs[index]++; } int32_t cum_move = 0; - for (unsigned int i = 0; i < x_pos.size(); ++i) { + for (size_t i = 0; i < x_pos.size(); ++i) { int index = line_id[i]; if (i == 0 || line_id[i-1] != index) { cum_move = 0; @@ -330,49 +307,100 @@ bool HarfBuzzShaper::finish_string() { x_pos[i] += cum_move; cum_move += (width - line_width[index]) / (n_glyphs[index]-1); } + pen_x += cum_move; + for (size_t i = 0; i < line_width.size(); ++i) { + if (n_glyphs[i] != 0) line_width[i] = width; + } } double width_diff = width - line_width[max_width_ind]; if (cur_align == 1) { width_diff /= 2; } - left_bearing = cur_align == 0 ? *std::min_element(line_left_bear.begin(), line_left_bear.end()) : line_left_bear[max_width_ind] + width_diff; - right_bearing = cur_align == 2 ? *std::min_element(line_right_bear.begin(), line_right_bear.end()) : line_right_bear[max_width_ind] + width_diff; - if (cur_hjust != 0.0) { - left_border = - cur_hjust * width; - pen_x += left_border; - for (unsigned int i = 0; i < x_pos.size(); ++i) { - x_pos[i] += left_border; - } + left_bearing = cur_align == 0 || cur_align == 3 || cur_align == 5 ? *std::min_element(line_left_bear.begin(), line_left_bear.end()) : line_left_bear[max_width_ind] + width_diff; + right_bearing = cur_align == 2 || cur_align == 4 || cur_align == 5 ? *std::min_element(line_right_bear.begin(), line_right_bear.end()) : line_right_bear[max_width_ind] + width_diff; + + left_border = - cur_hjust * width; + pen_x += left_border; + for (size_t i = 0; i < x_pos.size(); ++i) { + x_pos[i] += left_border; } - if (cur_vjust != 1.0) { - int32_t just_height = top_border - pen_y; - for (unsigned int i = 0; i < x_pos.size(); ++i) { - y_pos[i] += - pen_y - cur_vjust * just_height; - } - top_border += - pen_y - cur_vjust * just_height; - pen_y += - pen_y - cur_vjust * just_height; + for (size_t i = 0; i < x_pos.size(); ++i) { + y_pos[i] += - bottom - cur_vjust * height; } + top_border += - bottom - cur_vjust * height; + pen_y += - bottom - cur_vjust * height; return true; } -bool HarfBuzzShaper::single_line_shape(const char* string, FontSettings font_info, - double size, double res) { +void HarfBuzzShaper::reset() { + glyph_id.clear(); + glyph_cluster.clear(); + fontfile.clear(); + fontindex.clear(); + fontsize.clear(); + string_id.clear(); + x_pos.clear(); + y_pos.clear(); + advance.clear(); + ascender.clear(); + descender.clear(); + line_left_bear.clear(); + line_right_bear.clear(); + line_width.clear(); + line_id.clear(); + must_break.clear(); + may_stretch.clear(); + shape_infos.clear(); + + pen_x = 0; + pen_y = 0; + + top = 0; + bottom = 0; + ascend = 0; + descend = 0; + + left_bearing = 0; + right_bearing = 0; + top_bearing = 0; + bottom_bearing = 0; + width = 0; + height = 0; + top_border = 0; + left_border = 0; + + cur_string = 0; + + error_code = 0; + + cur_lineheight = 0.0; + cur_align = 0; + cur_hjust = 0.0; + cur_vjust = 0.0; + cur_res = 0.0; + top = 0; + bottom = 0; + max_width = 0; + indent = 0; + hanging = 0; + space_before = 0; + space_after = 0; +} + +ShapeInfo HarfBuzzShaper::shape_text_run(const char* string, FontSettings& font_info, + double size, double res, double tracking) { int n_features = font_info.n_features; std::vector features(n_features); + ShapeInfo text_run; + ShapeID run_id; if (n_features == 0) { - temp_shape_id.string.assign(string); - temp_shape_id.font.assign(font_info.file); - temp_shape_id.index = font_info.index; - temp_shape_id.size = size * res; - if (temp_shape_id == last_shape_id) { - return true; - } - if (shape_cache.get(temp_shape_id, last_shape_info)) { - last_shape_id.string.swap(temp_shape_id.string); - last_shape_id.font.swap(temp_shape_id.font); - last_shape_id.index = temp_shape_id.index; - last_shape_id.size = temp_shape_id.size; - return true; + run_id.string.assign(string); + run_id.font.assign(font_info.file); + run_id.index = font_info.index; + run_id.size = size * res; + run_id.tracking = tracking; + if (shape_cache.get(run_id, text_run)) { + return text_run; } } else { for (int i = 0; i < n_features; ++i) { @@ -382,16 +410,33 @@ bool HarfBuzzShaper::single_line_shape(const char* string, FontSettings font_inf features[i].start = 0; features[i].end = -1; } - // Reset temp id so we don't haphazardly use a shaping with features - temp_shape_id.string.clear(); - temp_shape_id.font.clear(); - temp_shape_id.index = 0; - temp_shape_id.size = 0; } + text_run.fallbacks.push_back(font_info); + int n_chars = 0; const uint32_t* utc_string = utf_converter.convert_to_ucs(string, n_chars); + if (n_chars == 0) { + text_run.ascenders.push_back(0); + text_run.descenders.push_back(0); +#if HB_VERSION_MAJOR < 2 && HB_VERSION_MINOR < 2 +#else + int error = 0; + hb_font_t *font = hb_ft_font_create(get_cached_face(font_info.file, font_info.index, size, res, &error), NULL); + if (error != 0) { + Rprintf("Failed to get face: %s, %i\n", font_info.file, font_info.index); + error_code = error; + } else { + hb_font_extents_t fextent; + hb_font_get_h_extents(font, &fextent); + text_run.ascenders[0] = fextent.ascender; + text_run.descenders[0] = fextent.descender; + } +#endif + return text_run; + } + std::vector embeddings = {}; if (n_chars > 1) { @@ -404,14 +449,6 @@ bool HarfBuzzShaper::single_line_shape(const char* string, FontSettings font_inf embeddings.push_back(0); } - last_shape_info.x_pos.clear(); - last_shape_info.glyph_id.clear(); - last_shape_info.width = 0; - last_shape_info.font.clear(); - last_shape_info.fallbacks.clear(); - last_shape_info.fallbacks.push_back(font_info); - last_shape_info.fallback_scaling.clear(); - bool may_have_emoji = false; for (int i = 0; i < n_chars; ++i) { if (utc_string[i] >= 8205) { @@ -428,7 +465,7 @@ bool HarfBuzzShaper::single_line_shape(const char* string, FontSettings font_inf if (emoji_embeddings[i] == 1) { embeddings[i] = 2; if (!emoji_font_added) { - last_shape_info.fallbacks.push_back(locate_font_with_features("emoji", 0, 0)); + text_run.fallbacks.push_back(locate_font_with_features("emoji", 0, 0)); emoji_font_added = true; } } @@ -438,130 +475,24 @@ bool HarfBuzzShaper::single_line_shape(const char* string, FontSettings font_inf size_t embedding_start = 0; for (size_t i = 1; i <= embeddings.size(); ++i) { if (i == embeddings.size() || embeddings[i] != embeddings[i - 1]) { - shape_embedding(utc_string, embedding_start, i, n_chars, size, res, features, embeddings[embedding_start] == 2); + bool success = shape_embedding(utc_string, embedding_start, i, n_chars, size, res, tracking, features, embeddings[embedding_start] == 2, text_run); + if (!success) return ShapeInfo(); embedding_start = i; } } - shape_cache.add(temp_shape_id, last_shape_info); - last_shape_id.string.swap(temp_shape_id.string); - last_shape_id.font.swap(temp_shape_id.font); - last_shape_id.index = temp_shape_id.index; - last_shape_id.size = temp_shape_id.size; - //FT_Done_Face(face); - return true; -} - -void HarfBuzzShaper::reset() { - glyph_id.clear(); - glyph_cluster.clear(); - string_id.clear(); - x_pos.clear(); - y_pos.clear(); - x_mid.clear(); - x_advance.clear(); - x_offset.clear(); - left_bear.clear(); - right_bear.clear(); - top_extend.clear(); - bottom_extend.clear(); - line_left_bear.clear(); - line_right_bear.clear(); - line_width.clear(); - line_id.clear(); - ascenders.clear(); - descenders.clear(); - may_break.clear(); - must_break.clear(); - may_stretch.clear(); - - pen_x = 0; - pen_y = 0; - - top = 0; - bottom = 0; - ascend = 0; - descend = 0; - - left_bearing = 0; - right_bearing = 0; - width = 0; - height = 0; - top_border = 0; - left_border = 0; - - cur_string = 0; -} - -bool HarfBuzzShaper::shape_glyphs(hb_font_t *font, const uint32_t *string, unsigned int n_chars) { - hb_shape(font, buffer, NULL, 0); - - unsigned int n_glyphs = 0; - hb_glyph_info_t *glyph_info = hb_buffer_get_glyph_infos(buffer, &n_glyphs); - hb_glyph_position_t *glyph_pos = hb_buffer_get_glyph_positions(buffer, &n_glyphs); - - if (n_glyphs == 0) return true; - - glyph_id.reserve(n_glyphs); - glyph_cluster.reserve(n_glyphs); - string_id.reserve(n_glyphs); - x_pos.reserve(n_glyphs); - y_pos.reserve(n_glyphs); - -#if HB_VERSION_MAJOR < 2 && HB_VERSION_MINOR < 2 - ascend = 0; - descend = 0; -#else - hb_font_extents_t extent; - hb_font_get_h_extents(font, &extent); - - ascend = extent.ascender; - descend = extent.descender; -#endif - - for (unsigned int i = 0; i < n_glyphs; ++i) { - unsigned int cluster = glyph_info[i].cluster; - glyph_cluster.push_back(cluster); - glyph_id.push_back(glyph_info[i].codepoint); - - if (cluster < n_chars) { - may_break.push_back(glyph_is_breaker(string[cluster])); - must_break.push_back(glyph_is_linebreak(string[cluster])); - may_stretch.push_back(glyph_may_stretch(string[cluster])); - } else { - may_break.push_back(false); - must_break.push_back(false); - may_stretch.push_back(false); - } - - string_id.push_back(cur_string); - - hb_glyph_extents_t extent; - hb_font_get_glyph_extents(font, glyph_info[i].codepoint, &extent); - - int32_t x_off = glyph_pos[i].x_offset; - int32_t x_adv = glyph_pos[i].x_advance; - x_advance.push_back(x_adv + cur_tracking); - left_bear.push_back(extent.x_bearing); - right_bear.push_back(x_adv - (extent.x_bearing + extent.width)); - top_extend.push_back(extent.y_bearing); - bottom_extend.push_back(extent.height + extent.y_bearing); - ascenders.push_back(ascend); - descenders.push_back(descend); - if (i == 0) { - x_offset.push_back(0); - } else { - x_offset.push_back(x_off); - } + if (n_features == 0) { + shape_cache.add(run_id, text_run); } - return true; + //FT_Done_Face(face); + return text_run; } bool HarfBuzzShaper::shape_embedding(const uint32_t* string, unsigned start, unsigned end, unsigned int string_length, - double size, double res, + double size, double res, double tracking, std::vector& features, - bool emoji) { + bool emoji, ShapeInfo& shape_info) { unsigned int embedding_size = end - start; if (embedding_size < 1) { @@ -569,36 +500,38 @@ bool HarfBuzzShaper::shape_embedding(const uint32_t* string, unsigned start, } int error = 0; - FT_Face face = get_cached_face(last_shape_info.fallbacks[0].file, - last_shape_info.fallbacks[0].index, + FT_Face face = get_cached_face(shape_info.fallbacks[0].file, + shape_info.fallbacks[0].index, size, res, &error); if (error != 0) { - Rprintf("Failed to get face: %s, %i\n", last_shape_info.fallbacks[0].file, last_shape_info.fallbacks[0].index); + Rprintf("Failed to get face: %s, %i\n", shape_info.fallbacks[0].file, shape_info.fallbacks[0].index); error_code = error; return false; } - if (last_shape_info.fallback_scaling.empty()) { - double scaling = FT_IS_SCALABLE(face) ? -1 : size * 64.0 / face->size->metrics.height; - scaling *= family_scaling(face->family_name); - last_shape_info.fallback_scaling.push_back(scaling); + if (shape_info.fallback_scaling.empty()) { + double scaling = FT_IS_SCALABLE(face) ? -1 : size * 64.0 * res / 72.0 / face->size->metrics.height; + double fscaling = family_scaling(face->family_name); + shape_info.fallback_scaling.push_back(scaling * fscaling); + shape_info.fallback_size.push_back(size * fscaling); } if (emoji) { - face = get_cached_face(last_shape_info.fallbacks[1].file, - last_shape_info.fallbacks[1].index, + face = get_cached_face(shape_info.fallbacks[1].file, + shape_info.fallbacks[1].index, size, res, &error); if (error != 0) { - Rprintf("Failed to get face: %s, %i\n", last_shape_info.fallbacks[1].file, last_shape_info.fallbacks[1].index); + Rprintf("Failed to get emoji face: %s, %i\n", shape_info.fallbacks[1].file, shape_info.fallbacks[1].index); error_code = error; return false; } - if (last_shape_info.fallback_scaling.size() == 1) { - double scaling = FT_IS_SCALABLE(face) ? -1 : size * 64.0 / face->size->metrics.height; - scaling *= family_scaling(face->family_name); - last_shape_info.fallback_scaling.push_back(scaling); + if (shape_info.fallback_scaling.size() == 1) { + double scaling = FT_IS_SCALABLE(face) ? -1 : size * 64.0 * res / 72.0 / face->size->metrics.height; + double fscaling = family_scaling(face->family_name); + shape_info.fallback_scaling.push_back(scaling * fscaling); + shape_info.fallback_size.push_back(size * fscaling); } } @@ -608,10 +541,11 @@ bool HarfBuzzShaper::shape_embedding(const uint32_t* string, unsigned start, hb_buffer_reset(buffer); hb_buffer_add_utf32(buffer, string, string_length, start, embedding_size); hb_buffer_guess_segment_properties(buffer); + hb_glyph_info_t *glyph_info = NULL; + glyph_info = hb_buffer_get_glyph_infos(buffer, &n_glyphs); hb_shape(font, buffer, features.data(), features.size()); - hb_glyph_info_t *glyph_info = NULL; hb_glyph_position_t *glyph_pos = NULL; glyph_info = hb_buffer_get_glyph_infos(buffer, &n_glyphs); @@ -620,12 +554,12 @@ bool HarfBuzzShaper::shape_embedding(const uint32_t* string, unsigned start, return true; } - bool ltr = true; + shape_info.ltr = true; for (unsigned int i = 1; i < n_glyphs; ++i) { if (glyph_info[i - 1].cluster == glyph_info[i].cluster) { continue; } - ltr = glyph_info[i - 1].cluster < glyph_info[i].cluster; + shape_info.ltr = glyph_info[i - 1].cluster < glyph_info[i].cluster; break; } @@ -633,11 +567,12 @@ bool HarfBuzzShaper::shape_embedding(const uint32_t* string, unsigned start, std::vector char_font(embedding_size, current_font); bool needs_fallback = false; bool any_resolved = false; - annotate_fallbacks(current_font + 1, 0, char_font, glyph_info, n_glyphs, needs_fallback, any_resolved, ltr, start); + annotate_fallbacks(current_font + 1, 0, char_font, glyph_info, n_glyphs, needs_fallback, any_resolved, shape_info.ltr, start); if (!needs_fallback) { // Short route - use existing shaping glyph_pos = hb_buffer_get_glyph_positions(buffer, &n_glyphs); - fill_shape_info(glyph_info, glyph_pos, n_glyphs, font, current_font); + fill_shape_info(glyph_info, glyph_pos, n_glyphs, font, current_font, start, shape_info, tracking); + fill_glyph_info(string, start + embedding_size, shape_info); hb_font_destroy(font); return true; } @@ -659,13 +594,13 @@ bool HarfBuzzShaper::shape_embedding(const uint32_t* string, unsigned start, } int error = 0; bool using_new = false; - font = load_fallback(current_font, string, start + fallback_start, start + fallback_end, error, size, res, using_new); + font = load_fallback(current_font, string, start + fallback_start, start + fallback_end, error, size, res, using_new, shape_info); if (!using_new) { // We don't need to have success if we are trying an existing font any_resolved = true; } if (error != 0) { - Rprintf("Failed to get face: %s, %i\n", last_shape_info.fallbacks[current_font].file, last_shape_info.fallbacks[current_font].index); + Rprintf("Failed to get face: %s, %i\n", shape_info.fallbacks[current_font].file, shape_info.fallbacks[current_font].index); error_code = error; return false; } @@ -679,7 +614,7 @@ bool HarfBuzzShaper::shape_embedding(const uint32_t* string, unsigned start, if (n_glyphs > 0) { bool needs_fallback_2 = false; bool any_resolved_2 = false; - annotate_fallbacks(current_font + 1, fallback_start, char_font, glyph_info, n_glyphs, needs_fallback_2, any_resolved_2, ltr, start); + annotate_fallbacks(current_font + 1, fallback_start, char_font, glyph_info, n_glyphs, needs_fallback_2, any_resolved_2, shape_info.ltr, start); if (needs_fallback_2) needs_fallback = true; if (any_resolved_2) any_resolved = true; } @@ -692,22 +627,22 @@ bool HarfBuzzShaper::shape_embedding(const uint32_t* string, unsigned start, // Make sure char_font does not point to non-existing fonts for (size_t i = 0; i < char_font.size(); ++i) { - if (char_font[i] >= last_shape_info.fallbacks.size()) { + if (char_font[i] >= shape_info.fallbacks.size()) { char_font[i] = 0; } } - if (ltr) { + if (shape_info.ltr) { current_font = char_font[0]; unsigned int text_run_start = 0; for (unsigned int i = 1; i <= embedding_size; ++i) { if (i == embedding_size || char_font[i] != current_font) { int error = 0; - FT_Face face = get_cached_face(last_shape_info.fallbacks[current_font].file, - last_shape_info.fallbacks[current_font].index, + FT_Face face = get_cached_face(shape_info.fallbacks[current_font].file, + shape_info.fallbacks[current_font].index, size, res, &error); if (error != 0) { - Rprintf("Failed to get face: %s, %i\n", last_shape_info.fallbacks[current_font].file, last_shape_info.fallbacks[current_font].index); + Rprintf("Failed to get face: %s, %i\n", shape_info.fallbacks[current_font].file, shape_info.fallbacks[current_font].index); error_code = error; return false; } @@ -720,12 +655,13 @@ bool HarfBuzzShaper::shape_embedding(const uint32_t* string, unsigned start, hb_shape(font, buffer, features.data(), features.size()); glyph_info = hb_buffer_get_glyph_infos(buffer, &n_glyphs); glyph_pos = hb_buffer_get_glyph_positions(buffer, &n_glyphs); - fill_shape_info(glyph_info, glyph_pos, n_glyphs, font, current_font); + fill_shape_info(glyph_info, glyph_pos, n_glyphs, font, current_font, start + text_run_start, shape_info, tracking); + fill_glyph_info(string, start + i, shape_info); hb_font_destroy(font); if (i < embedding_size) { current_font = char_font[i]; - if (current_font >= last_shape_info.fallbacks.size()) current_font = 0; + if (current_font >= shape_info.fallbacks.size()) current_font = 0; text_run_start = i; } } @@ -736,11 +672,11 @@ bool HarfBuzzShaper::shape_embedding(const uint32_t* string, unsigned start, for (int i = text_run_end - 1; i >= 0; --i) { if (i <= 0 || char_font[i - 1] != current_font) { int error = 0; - FT_Face face = get_cached_face(last_shape_info.fallbacks[current_font].file, - last_shape_info.fallbacks[current_font].index, + FT_Face face = get_cached_face(shape_info.fallbacks[current_font].file, + shape_info.fallbacks[current_font].index, size, res, &error); if (error != 0) { - Rprintf("Failed to get face: %s, %i\n", last_shape_info.fallbacks[current_font].file, last_shape_info.fallbacks[current_font].index); + Rprintf("Failed to get face: %s, %i\n", shape_info.fallbacks[current_font].file, shape_info.fallbacks[current_font].index); error_code = error; return false; } @@ -753,22 +689,71 @@ bool HarfBuzzShaper::shape_embedding(const uint32_t* string, unsigned start, hb_shape(font, buffer, features.data(), features.size()); glyph_info = hb_buffer_get_glyph_infos(buffer, &n_glyphs); glyph_pos = hb_buffer_get_glyph_positions(buffer, &n_glyphs); - fill_shape_info(glyph_info, glyph_pos, n_glyphs, font, current_font); + fill_shape_info(glyph_info, glyph_pos, n_glyphs, font, current_font, start + i, shape_info, tracking); + fill_glyph_info(string, start + text_run_end, shape_info); hb_font_destroy(font); if (i > 0) { current_font = char_font[i - 1]; - if (current_font >= last_shape_info.fallbacks.size()) current_font = 0; + if (current_font >= shape_info.fallbacks.size()) current_font = 0; text_run_end = i; } } } } - return true; } +hb_font_t* HarfBuzzShaper::load_fallback(unsigned int font, const uint32_t* string, unsigned int start, unsigned int end, int& error, double size, double res, bool& new_added, ShapeInfo& shape_info) { + + new_added = false; + if (font >= shape_info.fallbacks.size()) { + int n_conv = 0; + const char* fallback_string = utf_converter.convert_to_utf(string + start, end - start, n_conv); + shape_info.fallbacks.push_back( + get_fallback(fallback_string, + shape_info.fallbacks[0].file, + shape_info.fallbacks[0].index) + ); + new_added = true; + } + FT_Face face = get_cached_face(shape_info.fallbacks[font].file, + shape_info.fallbacks[font].index, + size, res, &error); + + if (error != 0) { + return NULL; + } + + if (font >= shape_info.fallback_scaling.size()) { + double scaling = FT_IS_SCALABLE(face) ? -1 : size * 64.0 * res / 72.0 / face->size->metrics.height; + double fscaling = family_scaling(face->family_name); + shape_info.fallback_scaling.push_back(scaling * fscaling); + shape_info.fallback_size.push_back(size * fscaling); + } + + return hb_ft_font_create(face, NULL); +} + +bool HarfBuzzShaper::fallback_cluster(unsigned int font, std::vector& char_font, unsigned int from, unsigned int& start, unsigned int& end) { + bool has_cluster = false; + for (unsigned int i = from; i < char_font.size(); ++i) { + if (char_font[i] == font) { + start = i; + has_cluster = true; + break; + } + } + for (unsigned int i = start + 1; i <= char_font.size(); ++i) { + if (i == char_font.size() || char_font[i] != font) { + end = i; + break; + } + } + return has_cluster; +} + void HarfBuzzShaper::annotate_fallbacks(unsigned int font, unsigned int offset, std::vector& char_font, hb_glyph_info_t* glyph_info, @@ -811,73 +796,153 @@ void HarfBuzzShaper::annotate_fallbacks(unsigned int font, unsigned int offset, void HarfBuzzShaper::fill_shape_info(hb_glyph_info_t* glyph_info, hb_glyph_position_t* glyph_pos, unsigned int n_glyphs, hb_font_t* font, - unsigned int font_id) { - double scaling = last_shape_info.fallback_scaling[font_id]; + unsigned int font_id, + unsigned int cluster_offset, + ShapeInfo& shape_info, int32_t tracking) { + double scaling = shape_info.fallback_scaling[font_id]; if (scaling < 0) scaling = 1.0; + + tracking *= shape_info.fallback_size[font_id] / 1000; + + +#if HB_VERSION_MAJOR < 2 && HB_VERSION_MINOR < 2 + ascend = 0; + descend = 0; +#else + hb_font_extents_t fextent; + hb_font_get_h_extents(font, &fextent); + + ascend = fextent.ascender; + descend = fextent.descender; +#endif + hb_glyph_extents_t extent; - int32_t x = last_shape_info.width; + + int new_size = shape_info.glyph_id.size() + n_glyphs; + shape_info.glyph_id.reserve(new_size); + shape_info.glyph_cluster.reserve(new_size); + shape_info.x_offset.reserve(new_size); + shape_info.y_offset.reserve(new_size); + shape_info.x_advance.reserve(new_size); + shape_info.y_advance.reserve(new_size); + shape_info.x_bear.reserve(new_size); + shape_info.y_bear.reserve(new_size); + shape_info.width.reserve(new_size); + shape_info.height.reserve(new_size); + shape_info.ascenders.reserve(new_size); + shape_info.descenders.reserve(new_size); + shape_info.font.reserve(new_size); + for (unsigned int i = 0; i < n_glyphs; ++i) { - if (last_shape_info.x_pos.empty()) { - hb_font_get_glyph_extents(font, glyph_info[i].codepoint, &extent); - last_shape_info.left_bearing = extent.x_bearing * scaling; - } - last_shape_info.x_pos.push_back(x + (glyph_pos[i].x_offset) * scaling); - last_shape_info.glyph_id.push_back(glyph_info[i].codepoint); - last_shape_info.font.push_back(font_id); - x += glyph_pos[i].x_advance * scaling; - } - last_shape_info.width = x; - hb_font_get_glyph_extents(font, glyph_info[n_glyphs - 1].codepoint, &extent); - last_shape_info.right_bearing = glyph_pos[n_glyphs - 1].x_advance - (extent.x_bearing + extent.width); - last_shape_info.right_bearing *= scaling; -} + shape_info.glyph_id.push_back(glyph_info[i].codepoint); + shape_info.glyph_cluster.push_back(glyph_info[i].cluster); + shape_info.x_offset.push_back(glyph_pos[i].x_offset * scaling); + shape_info.y_offset.push_back(glyph_pos[i].y_offset * scaling); + shape_info.x_advance.push_back(glyph_pos[i].x_advance * scaling + tracking); + shape_info.y_advance.push_back(glyph_pos[i].y_advance * scaling); -hb_font_t* HarfBuzzShaper::load_fallback(unsigned int font, const uint32_t* string, unsigned int start, unsigned int end, int& error, double size, double res, bool& new_added) { + hb_font_get_glyph_extents(font, glyph_info[i].codepoint, &extent); + shape_info.x_bear.push_back(extent.x_bearing * scaling); + shape_info.y_bear.push_back(extent.y_bearing * scaling); + shape_info.width.push_back(extent.width * scaling); + shape_info.height.push_back(extent.height * scaling); - new_added = false; - if (font >= last_shape_info.fallbacks.size()) { - int n_conv = 0; - const char* fallback_string = utf_converter.convert_to_utf(string + start, end - start, n_conv); - last_shape_info.fallbacks.push_back( - get_fallback(fallback_string, - last_shape_info.fallbacks[0].file, - last_shape_info.fallbacks[0].index) - ); - new_added = true; - } - FT_Face face = get_cached_face(last_shape_info.fallbacks[font].file, - last_shape_info.fallbacks[font].index, - size, res, &error); + shape_info.ascenders.push_back(ascend * scaling); + shape_info.descenders.push_back(descend * scaling); - if (error != 0) { - return NULL; - } + shape_info.font.push_back(font_id); - if (font >= last_shape_info.fallback_scaling.size()) { - double scaling = FT_IS_SCALABLE(face) ? -1 : size * 64.0 / face->size->metrics.height; - scaling *= family_scaling(face->family_name); - last_shape_info.fallback_scaling.push_back(scaling); } - - return hb_ft_font_create(face, NULL); } -bool HarfBuzzShaper::fallback_cluster(unsigned int font, std::vector& char_font, unsigned int from, unsigned int& start, unsigned int& end) { - bool has_cluster = false; - for (unsigned int i = from; i < char_font.size(); ++i) { - if (char_font[i] == font) { - start = i; - has_cluster = true; - break; +void HarfBuzzShaper::fill_glyph_info(const uint32_t* string, int end, + ShapeInfo& shape_info) { + for (size_t i = shape_info.must_break.size(); i < shape_info.glyph_cluster.size(); ++i) { + int32_t cluster = shape_info.glyph_cluster[i]; + if (cluster < end) { + shape_info.must_break.push_back(glyph_is_linebreak(string[cluster])); + shape_info.may_break.push_back(glyph_is_breaker(string[cluster])); + shape_info.may_stretch.push_back(glyph_may_stretch(string[cluster])); + } else { + shape_info.must_break.push_back(false); + shape_info.may_break.push_back(false); + shape_info.may_stretch.push_back(false); } } - for (unsigned int i = start + 1; i <= char_font.size(); ++i) { - if (i == char_font.size() || char_font[i] != font) { - end = i; - break; +} + +size_t HarfBuzzShaper::fill_out_width(size_t from, int32_t max, + size_t shape, int& breaktype) { + int32_t w = 0; + size_t last_possible_break = from; + bool has_break = false; + breaktype = 0; + if (shape_infos[shape].ltr) { + if (max < 0) return shape_infos[shape].glyph_id.size(); + for (size_t i = from; i < shape_infos[shape].glyph_id.size(); ++i) { + if (shape_infos[shape].must_break[i]) { + breaktype = 2; + return i + 1; + } + if (shape_infos[shape].may_break[i]) { + last_possible_break = i; + has_break = true; + } + w += shape_infos[shape].x_advance[i]; + if (w > max) { + breaktype = 1; + return has_break ? last_possible_break + 1 : i; + } } + size_t next_shape = shape + 1; + while (next_shape < shape_infos.size()) { + for (size_t i = 0; i < shape_infos[next_shape].glyph_id.size(); ++i) { + if (shape_infos[next_shape].must_break[i] || shape_infos[next_shape].may_break[i]) { + return shape_infos[shape].glyph_id.size(); + } + w += shape_infos[next_shape].x_advance[i]; + if (w > max) { + breaktype = has_break ? 1 : 0; + return has_break ? last_possible_break + 1 : shape_infos[shape].glyph_id.size(); + } + } + next_shape++; + } + last_possible_break = shape_infos[shape].glyph_id.size(); + } else { + if (max < 0) return 0; + for (size_t i = from - 1; i >= 0; --i) { + if (shape_infos[shape].must_break[i]) { + breaktype = 2; + return i + 1; + } + if (shape_infos[shape].may_break[i]) { + last_possible_break = i; + has_break = true; + } + w += shape_infos[shape].x_advance[i]; + if (w > max) { + breaktype = 1; + return has_break ? last_possible_break : i + 1; + } + } + size_t next_shape = shape + 1; + while (next_shape < shape_infos.size()) { + for (size_t i = shape_infos[next_shape].glyph_id.size() - 1; i >= 0; --i) { + if (shape_infos[next_shape].must_break[i] || shape_infos[shape].may_break[i]) { + return 0; + } + w += shape_infos[next_shape].x_advance[i]; + if (w > max) { + breaktype = has_break ? 1 : 0; + return has_break ? last_possible_break : 0; + } + } + next_shape++; + } + last_possible_break = 0; } - return has_cluster; + return last_possible_break; } #endif diff --git a/src/string_shape.h b/src/string_shape.h index d5722ff..e83f508 100644 --- a/src/string_shape.h +++ b/src/string_shape.h @@ -16,35 +16,51 @@ struct ShapeID { std::string font; unsigned int index; double size; + double tracking; - inline ShapeID() : string(""), font(""), index(0), size(0.0) {} - inline ShapeID(std::string _string, std::string _font, unsigned int _index, double _size) : + inline ShapeID() : string(""), font(""), index(0), size(0.0), tracking(0.0) {} + inline ShapeID(std::string _string, std::string _font, unsigned int _index, double _size, double _tracking) : string(_string), font(_font), index(_index), - size(_size) {} + size(_size), + tracking(_tracking) {} inline ShapeID(const ShapeID& shape) : string(shape.string), font(shape.font), index(shape.index), - size(shape.size) {} + size(shape.size), + tracking(shape.tracking) {} inline bool operator==(const ShapeID &other) const { return (index == other.index && size == other.size && string == other.string && - font == other.font); + font == other.font) && + tracking == other.tracking; } }; struct ShapeInfo { std::vector glyph_id; - std::vector x_pos; + std::vector glyph_cluster; + std::vector x_advance; + std::vector y_advance; + std::vector x_offset; + std::vector y_offset; + std::vector x_bear; + std::vector y_bear; + std::vector width; + std::vector height; + std::vector ascenders; + std::vector descenders; + std::vector may_break; + std::vector must_break; + std::vector may_stretch; std::vector font; std::vector fallbacks; + std::vector fallback_size; std::vector fallback_scaling; - int32_t width; - int32_t left_bearing; - int32_t right_bearing; + bool ltr; }; namespace std { template <> @@ -53,7 +69,8 @@ struct hash { return std::hash()(x.string) ^ std::hash()(x.font) ^ std::hash()(x.index) ^ - std::hash()(x.size); + std::hash()(x.size) ^ + std::hash()(x.tracking); } }; } @@ -61,6 +78,19 @@ struct hash { class HarfBuzzShaper { public: HarfBuzzShaper() : + // Public + glyph_id(), + glyph_cluster(), + fontfile(), + fontindex(), + fontsize(), + string_id(), + x_pos(), + y_pos(), + advance(), + ascender(), + descender(), + must_break(), width(0), height(0), left_bearing(0), @@ -72,12 +102,15 @@ class HarfBuzzShaper { pen_x(0), pen_y(0), error_code(0), + // Private cur_lineheight(0.0), cur_align(0), cur_string(0), cur_hjust(0.0), cur_vjust(0.0), cur_res(0.0), + shape_infos(), + may_stretch(), line_left_bear(), line_right_bear(), line_width(), @@ -98,12 +131,18 @@ class HarfBuzzShaper { hb_buffer_destroy(buffer); }; - static std::vector glyph_id; - static std::vector glyph_cluster; - static std::vector string_id; - static std::vector x_pos; - static std::vector y_pos; - static std::vector x_mid; + std::vector glyph_id; + std::vector glyph_cluster; + std::vector fontfile; + std::vector fontindex; + std::vector fontsize; + std::vector string_id; + std::vector x_pos; + std::vector y_pos; + std::vector advance; + std::vector ascender; + std::vector descender; + std::vector must_break; int32_t width; int32_t height; int32_t left_bearing; @@ -114,29 +153,26 @@ class HarfBuzzShaper { int32_t left_border; int32_t pen_x; int32_t pen_y; - static ShapeInfo last_shape_info; int error_code; - bool shape_string(const char* string, const char* fontfile, int index, + bool shape_string(const char* string, FontSettings& font_info, double size, double res, double lineheight, int align, double hjust, double vjust, double width, double tracking, double ind, double hang, double before, - double after); - bool add_string(const char* string, const char* fontfile, int index, - double size, double tracking); + double after, bool spacer); + bool add_string(const char* string, FontSettings& font_info, + double size, double tracking, bool spacer); + bool add_spacer(double height, double width); bool finish_string(); - - bool single_line_shape(const char* string, FontSettings font_info, double size, - double res); + ShapeInfo shape_text_run(const char* string, FontSettings& font_info, double size, + double res, double tracking); private: static UTF_UCS utf_converter; static LRU_Cache > bidi_cache; static LRU_Cache shape_cache; - static ShapeID last_shape_id; - static ShapeID temp_shape_id; hb_buffer_t *buffer; double cur_lineheight; int cur_align; @@ -144,18 +180,8 @@ class HarfBuzzShaper { double cur_hjust; double cur_vjust; double cur_res; - double cur_tracking; - static std::vector x_advance; - static std::vector x_offset; - static std::vector left_bear; - static std::vector right_bear; - static std::vector top_extend; - static std::vector bottom_extend; - static std::vector ascenders; - static std::vector descenders; - static std::vector may_break; - static std::vector must_break; - static std::vector may_stretch; + std::vector shape_infos; + std::vector may_stretch; std::vector line_left_bear; std::vector line_right_bear; std::vector line_width; @@ -172,13 +198,14 @@ class HarfBuzzShaper { int32_t space_after; void reset(); - bool shape_glyphs(hb_font_t *font, const uint32_t *string, unsigned int n_chars); bool shape_embedding(const uint32_t* string, unsigned start, unsigned end, unsigned int string_length, double size, double res, - std::vector& features, bool emoji); + double tracking, std::vector& features, + bool emoji, ShapeInfo& shape_info); hb_font_t* load_fallback(unsigned int font, const uint32_t* string, unsigned int start, unsigned int end, int& error, - double size, double res, bool& new_added); + double size, double res, bool& new_added, + ShapeInfo& shape_info); bool fallback_cluster(unsigned int font, std::vector& char_font, unsigned int from, unsigned int& start, unsigned int& end); void annotate_fallbacks(unsigned int font, unsigned int offset, @@ -187,7 +214,11 @@ class HarfBuzzShaper { bool& needs_fallback, bool& any_resolved, bool ltr, unsigned int string_offset); void fill_shape_info(hb_glyph_info_t* glyph_info, hb_glyph_position_t* glyph_pos, - unsigned int n_glyphs, hb_font_t* font, unsigned int font_id); + unsigned int n_glyphs, hb_font_t* font, unsigned int font_id, + unsigned int cluster_offset, ShapeInfo& shape_info, + int32_t tracking); + void fill_glyph_info(const uint32_t* string, int end, ShapeInfo& shape_info); + size_t fill_out_width(size_t from, int32_t max, size_t shape, int& breaktype); inline double family_scaling(const char* family) { if (strcmp("Apple Color Emoji", family) == 0) { @@ -197,47 +228,50 @@ class HarfBuzzShaper { } return 1; } - inline bool glyph_is_linebreak(int id) { + inline bool glyph_is_linebreak(int32_t id) { switch (id) { - case 10: return true; - case 11: return true; - case 12: return true; - case 13: return true; - case 133: return true; - case 8232: return true; - case 8233: return true; + case 10: return true; // Line feed + case 11: return true; // Vertical tab + case 12: return true; // Form feed + case 13: return true; // Cariage return + case 133: return true; // Next line + case 8232: return true; // Line Separator + case 8233: return true; // Paragraph Separator } return false; } - inline bool glyph_is_breaker(int id) { + inline bool glyph_is_breaker(int32_t id) { switch (id) { - case 9: return true; - case 32: return true; - case 5760: return true; - case 6158: return true; - case 8192: return true; - case 8193: return true; - case 8194: return true; - case 8195: return true; - case 8196: return true; - case 8197: return true; - case 8198: return true; - case 8200: return true; - case 8201: return true; - case 8202: return true; - case 8203: return true; - case 8204: return true; - case 8205: return true; - case 8287: return true; - case 12288: return true; + case 9: return true; // Horizontal tab + case 32: return true; // Space + case 45: return true; // Hyphen + case 173: return true; // Soft hyphen + case 5760: return true; // Ogham Space Mark + case 6158: return true; // Mongolian Vowel Separator + case 8192: return true; // En Quad + case 8193: return true; // Em Quad + case 8194: return true; // En Space + case 8195: return true; // Em Space + case 8196: return true; // Three-Per-Em Space + case 8197: return true; // Four-Per-Em Space + case 8198: return true; // Six-Per-Em Space + case 8200: return true; // Punctuation Space + case 8201: return true; // Thin Space + case 8202: return true; // Hair Space + case 8203: return true; // Zero Width Space + case 8204: return true; // Zero Width Non-Joiner + case 8205: return true; // Zero Width Joiner + case 8208: return true; // Hyphen + case 8287: return true; // Medium Mathematical Space + case 12288: return true; // Ideographic Space } return false; } - inline bool glyph_may_stretch(int id) { + inline bool glyph_may_stretch(int32_t id) { switch (id) { - case 32: return true; + case 32: return true; // Space } return false; }