by Paul Murrellhttp://orcid.org/0000-0002-3224-8858, Thomas Lin Pedersen, and Simon Urbanek.
Version 1: Saturday 06 May 2023
This document
is licensed under a Creative
Commons Attribution 4.0 International License.
This document describes a new feature in the R graphics engine to support rendering typeset glyphs. This provides a basis for developing improved text rendering in R graphics.
Most statistical plots contain text labels of some sort. For example, the plot below contains axis labels and tick mark labels.
plot(mpg ~ disp, mtcars, pch=21, bg=rgb(0,0,0,.3), cex=1.2)
In R (R Core Team, 2023), the user can typically provide character values to customise the labels. For example, the following code provides more readable axis labels.
plot(mpg ~ disp, mtcars, pch=21, bg=rgb(0,0,0,.3), cex=1.2, xlab="Displacement", ylab="Miles per Gallon")
In addition, R graphics provides several graphical parameters that control the appearance of text, such as the font family, the font face, the font size, and the colour. For example, the following code makes the tick labels and the axis labels bold and grey.
plot(mpg ~ disp, mtcars, pch=21, bg=rgb(0,0,0,.3), cex=1.2, xlab="Displacement", ylab="Miles per Gallon", font.axis=2, font.lab=2, col.axis="grey", col.lab="grey")
We can also draw individual text elements, with lower-level functions
like text()
. For example, the following code
draws a line from the point representing the Ferrari Dino to
a label. The label is drawn using the colour
"racing red"
(rosso corsa)
and the
text is left justified relative
to the x-value of the data point (and the data point itself is drawn red).
rossoCorsa <- "#D40000" plot(mpg ~ disp, mtcars, pch=21, bg=rgb(0,0,0,.3), cex=1.2, xlab="Displacement", ylab="Miles per Gallon", font.axis=2, font.lab=2, col.axis="grey", col.lab="grey") lines(mtcars$disp[30] - c(0, 40, 40, 0), mtcars$mpg[30] - c(0, 0, 7, 7)) points(mtcars$disp[30], mtcars$mpg[30], bg=rossoCorsa, pch=21) text(mtcars$disp[30] + 2, mtcars$mpg[30] - 7, rownames(mtcars)[30], col=rossoCorsa, adj=c(0))
All of the examples so far require only a very simple text-drawing model: all we need to specify is the text to draw, as a character value, where to draw it (including justification), and what font, colour, etc to use.
This model is reflected in the lowest-level user interface for
drawing text in the 'graphics' package, which is shown below:
x
and y
provide the location;
labels
provides the text to draw;
adj
, pos
, and offset
are all involved in the justification of the text; and
the remaining arguments control the font and colour selection.
text(x, y = NULL, labels = seq_along(x$x), adj = NULL, pos = NULL, offset = 0.5, vfont = NULL, cex = 1, col = NULL, font = NULL, ...)
The alternative low-level graphics system, the 'grid' package, provides a similar user interface.
grid.text(label, x = unit(0.5, "npc"), y = unit(0.5, "npc"), just = "centre", hjust = NULL, vjust = NULL, rot = 0, check.overlap = FALSE, default.units = "npc", name = NULL, gp = gpar(), draw = TRUE, vp = NULL)
This simple text model is also reflected in the interface between
the R graphics engine and graphics devices - the interface that is used by
developers of third-party graphics devices as well as for graphics
devices provided by R itself:
x
and y
provide the location,
str
is the text to draw,
rot
is an angle to draw the text at,
hadj
describes justification,
and gc
contains graphical parameter settings
(colours, fonts, etc).
dev->text(double x, double y, char *str, double rot, double hadj, pGEcontext gc);
These interfaces are important because they place limits on what the user is able to draw (easily) with R. As a simple example of something that we cannot (easily) do with R, in the plot below we have drawn the label for the Ferrari Dino with just the word "Ferrari" in racing red and using a bold-italic font; the word "Dino" is drawn (plain) black.
This simple modification is not (easily) possible with the text-drawing interface in R because, in the interfaces shown above, we can only specify a single colour and font face for each character value.
In addition to the limitations that the user interface imposes
on what users are able to
express with R code, the graphics device interface also limits
what graphics device authors are able to provide.
For example, the PDF language (Adobe Systems Incorporated, 2008) is perfectly capable
of changing the colour and font of a word mid-sentence or
even mid-word, but the pdf()
graphics device cannot offer
that level of detail because the R graphics engine will
never request it.
This is just a simple example of the limitations of the core R graphics interface for text drawing. Some other limitations include:
Font specifications are limited to just a font family plus a font "face", where the choice is restricted to "plain", "bold", "italic", and "bold italic". We are unable to specify a numeric font "weight" (for example, CSS allows font weights from 100 to 900; World Wide Web Consortium, 2023a).
More generally, we are unable to select font variations such as condensed or access font features such as optional ligatures or number styles.
There is no support for line breaking. If we want to break text over more than one line, we must provide explicit new lines. More sophisticated typesetting, such as paragraphs with indents, are even further out of reach.
The goal of the changes to the R graphics engine that are described in this document is to provide a basis for more sophisticated text output. For example, the changes allow us (with the help of the 'textshaping' package) to layout a paragraph of text within a fixed width, with a mixture of font styles, as shown in the plot below. The plot shows the immigration statistics for Japan covering the period when the COVID pandemic began. The paragraph of text is a quote from the Japanese Minister of Land, Infrastructure, Transport and Tourism, Hiroshi Tabata.1
The plot below shows a more ambitious example where (with the help of the 'xdvir' package) we have added the same quote to the same plot, but this time in Japanese with the text typeset vertically (top-to-bottom and right-to-left).2 The y-axis label is also typeset vertically, which is a nice way to save horizontal space without forcing the viewer to turn their head 90 degrees.
The next section looks at what needs to happen in order to allow these more sophisticated results to be produced with R and establishes some important terminology. The Current offerings Section looks in more detail at what is currently possible with R and how close (or how far away) we are from sophisticated text output. The Section on Rendering typeset glyphs introduces the new interface that has been added to the R graphics engine (and some graphics devices) for drawing more sophisticated text. The Section on Generating typeset glyphs provides some examples of how the new interface can be built upon and then finally the Section on Integrating typeset glyphs describes additional details about the new interface.
There are two main tasks involved in more sophisticated text drawing:
Rich text input: We need to provide a way to include information about how text should be styled along with the text itself. Because the styling can change from word to word or even within a word, this typically requires some sort of markup where the styling is intermingled with the text, such as LaTeX (Lamport, 1986), HTML (WHATWG, 2023) and, to a lesser extent, markdown (Gruber, 2023). For example, the following code snippets demonstrate marked up text for both LaTeX and HTML/CSS where one word is red and the other is black (by default).
{\color{red}Ferrari} Dino
<span style="color: red">Ferrari</span> Dino
The information about styling will ideally include details about fonts, such as optional font features.
Typesetting glyphs: Given a set of rich text, we need to be able to determine which glyphs to use and where to draw them. This can become quite complex (Beingessner, 2019; Levien, 2020; World Wide Web Consortium, 2023b; World Wide Web Consortium, 2023c).
Rich text input typically involves characters, like "f" and "i", but what gets drawn are glyphs, which are shapes within a font. A simple example of the difference is the different appearance of the letters "f" and "i" in different fonts, e.g., a sans-serif font f and i versus a monospace font f and i. More complex examples inclue ligature substituion, where the two characters "f" and "i" result in a single "fi" ligature glyph.
In some circumstances, determining the placement of glyphs can be straightforward, with each glyph just drawn to the right of the previous one. However, certain combinations of glyphs may require fine adjustments known as kerning. For example, a lower-case "o" may be drawn further to the left if the preceding glyph is an upper-case "T". There is also the concept of tracking, whereby the spacing between all characters within a word are adjusted to either contract or expand the space taken up by the word. A more significant complication arises when we consider scripts other than English, where glyphs are drawn from right-to-left rather than left-to-right, and for some scripts the main text direction can be vertical rather than horizontal. Things get even more complicated if text consists of a combination of, for example, left-to-right and right-to-left text (bidirectional text).
If the text is to span more than a single line, typesetting also involves determining where to place line breaks. This can require adjustments to inter-word spacing in order to satisfy justification of the glyphs and may involve hyphenation if it is necessary to break a line in the middle of a word. Just determining possible locations for line breaks can be a challenge for some scripts that do not place any spaces between words. Further complications include things like indentation of the first line of a paragraph and the vertical spacing between lines (known as the leading of the text).
Another complication arises if the required glyphs are not available in the specified font. In this case, typesetting may involve font fallback, where the original font is replaced entirely (at least for some glyphs).
Many of these issues feed back into the issue of rich text input because information about, for example, tracking or leading requires the relevant styling parameters to be specified along with the text.
The basic text-drawing model in the Introduction was a bit of a straw man. There aleady exist several ways to get around the limitations of that model and get some of the more sophisticated text features in R.
In the simplest typesetting situations, for example a single line
of left-to-right text, all we require in order to
arrange text components next to each other
is the width of each text component.
R graphics provides the strwidth()
function
(and the graphics device API has dev->strWidth()
)
for this purpose.
In the 'grid' graphics system, there is a stringWidth()
function equivalent.
Graphics devices can also return more detailed character metrics
on a per-character basis (dev->metricInfo()
)
and there is a user-level interface in 'grid' (grobAscent()
and grobDescent()
).
With these functions, it is possible to draw text with different styles and arrange the text components side by side. It is even possible to implement a simple line-breaking algorithm, using spaces as possible break points. For example, the following code draws the word Ferrari in racing red and bold-italic face and then the word Dino in black, with the placement of the word Dino based on the width of the word Ferrari.
ferrari <- textGrob("Ferrari ", x=unit(5, "mm"), just="left", gp=gpar(col=rossoCorsa, fontface="bold.italic")) grid.draw(ferrari) grid.text("Dino", x=unit(5, "mm") + grobWidth(ferrari), just="left")
However, more sophisticated typesetting is at best a lot of additional work or else it is just not possible. For example, suppose that we want to typeset a word with a style change that occurs between two characters that would normally involve a kerning adjustment. Simply drawing the characters side by side is not a complete solution in that case.
The DIY situation is represented in the diagram below, which indicates that we can only provide blocks of plain text as input to R graphics and, while we could perform some simple arrangements of that text by querying the graphics device for text metrics, it would be very limited. In particular, we can only work with text characters, not glyphs.
In later diagrams, rich text input will be indicated by blue nodes and edges and typeset glyphs will be indicated by green nodes and edges.
The "plotmath" feature in R graphics (Murrell and Ihaka, 2000) provides both (limited) markup and (limited) typesetting for mathematical equations (based on the TeX algorithm; Knuth, 1986).
For example, the following code
describes text that involves a change in style
(the x
is italic),
plus a method for inputting characters that
are otherwise difficult to type as plain text
(the word mu
represents the Greek letter μ),
and non-left-to-right arrangement of text
(the μ is drawn above the 2
).
grid.text(expression(italic(x) - frac(mu, 2)))
R graphics can perform the typesetting of this rich text,
including deciding on which text characters to draw (e.g.,
mu
becomes μ) and calculating
where each character should be drawn (e.g., the placement of the
μ above the 2
).
The diagram below indicates (with blue) that this facility does provide a form of rich text input (in the form of R expressions), although that is quite generous because the possibilities for describing the styling of the output is very limited. However, the typesetting of the resulting mathematical equation still only works with text rather than with glyphs.
The other major limitation of this facility is that it is focused only on mathematical expressions. Additionally, the typesetting only really produces a nice result if a "mathematical" font is used (e.g., the Computer Modern fonts).
The 'gridtext' package (Wilke and Wiernik, 2022b) and its 'ggplot2' (Wickham, 2016) analogue, 'ggtext' (Wilke and Wiernik, 2022a), add basic rich text and basic typesetting on top of the standard R graphics facilities. With these packages, it is possible to describe rich text using a subset of markdown and HTML. The packages also perform the appropriate typesetting to draw the correct result. For example, the following code uses a combination of markdown and HTML (and CSS) markup to draw text with the word "Ferrari" in bold-italic red and "Dino" in black.
library(gridtext) richtext <- '<span style="color: #D40000">***Ferrari***</span> Dino' grid.draw(richtext_grob(richtext))
The nice thing about these packages is that they provide a convenient interface for describing rich text as well as typesetting the result. The diagram below indicates (with blue) that we again have genuine rich text input. However, the support for the markup languages in the input is only partial and the typesetting still deals in text values rather than glyphs (because of the restrictions imposed by the R graphics engine).
These packages represent a significant step up from the standard R graphics text drawing facilities. However, it is only possible to produce simple typesetting results.
The most significant contributions of the 'systemfonts' package (Pedersen et al., 2022), in terms of typesetting, are that it allows more detailed specification of fonts, including the selection of font features like optional ligatures, and it can perform font fallback, selecting a substitute font if a required glyph is not available in the user-specified font. The 'systemfonts' package also provides a typesetting facility, but that is overshadowed by the facility that is offered by its big brother, 'textshaping' (Pedersen, 2021).
The primary purpose of the 'textshaping' package is to perform typesetting. Rich text input is converted into a set of glyphs and their locations. With the help of 'systemfonts', this package handles multiple complex issues including ligatures, kerning, and text direction (including bidirectional text). The 'textshaping' package can also perform basic line breaking.
The limitation with 'textshaping' is the lack of a convenient way to describe rich text. At present, the only input format is a set of vectors of text and accompanying font information. The other problem is that R graphics cannot do anything with the raw glyph information that 'textshaping' generates. The graphics engine works with text, not glyphs.
The 'ragg' package (Pedersen and Shemanarev, 2022) does not perform typesetting itself, but it provides graphics devices that make use of both 'systemfonts' and 'textshaping', so are capable of sophisticated typesetting. Even though we cannot feed rich text input to these devices (because of the limitations of the R graphics engine), we can still see glimpses of what they can do. For example, the following code draws text written in Hebrew.3 The important point about this text is that it should be written from right-to-left. Even without any rich text mark up, the 'textshaping' package can detect the correct direction of the text and typeset it appropriately.
library(ragg) agg_png("hebrew-ragg.png", width=100, height=50, res=96) grid.text("פרארי דינו") dev.off()
By way of contrast, the code and image below show the result of attempting to draw this text on an X11 (non-Cairo) graphics device. Notice that the Hebrew is incorrectly drawn left-to-right.
png("hebrew-x11.png", type="Xlib", width=100, height=50, res=96) grid.text("פרארי דינו") dev.off()
The 'ragg' package benefits from direct use of 'textshaping' because it can send text to 'textshaping' and get back raw glyph information to draw.
The diagram below indicates (with green) that the 'ragg' package graphics devices, thanks to the 'textshaping' package, are able to perform serious typesetting operations involving glyphs. The limitation here is that 'ragg' can only receive input from the R graphics engine, so it cannot work with rich text (because the graphics engine only sends plain text to graphics devices). Another issue is that this only (currently) works for the 'ragg' devices, although other graphics devices could choose to make use of 'textshaping' if they wish.
The 'dvir' package (Murrell, 2023a) is an experimental package that is not on CRAN, and has only been tested on Linux, but it is included here to provide an alternative example of a package that can perform sophisticated typesetting.
The 'dvir' package takes LaTeX code as input, which means that it can work with very rich text, and it makes use of one the main TeX engines (pdftex or luatex; Thành, 2023; The LuaTeX team, 2023) to perform typesetting, which means that it can produce a very wide range of results (including hyphenation, multi-column text, not to mention mathematical equations). For example, the following code uses LaTeX markup to draw text with the word "Ferrari" in bold-italic red and "Dino" in black.
library(dvir) tex <- "\\sffamily\\definecolor{rossoCorsa}{RGB}{212,0,0}{\\color{rossoCorsa}\\bfseries\\itshape Ferrari} Dino" grid.lualatex(tex)
The output from the TeX typesetting is a DVI file, which is essentially a set of typeset glyph information. The 'dvir' package is able to read the glyph information from a DVI file, but it then hits difficulties because R graphics cannot work with typeset glyph information. The package has to do a lot of work to turn the glyph information from a DVI file back into text so that it can feed text into R graphics functions to draw the TeX result. There are also issues with making use of the detailed font information in a DVI file (given that R graphics can only cope with a simplistic font description).
An example of the glyph-to-text problem is that, while some glyphs can be represented in text, e.g., "fi" can be expressed as a UNICODE character "\uFB01", some glyphs have no recognised representation e.g., there is no UNICODE code point for the "ti" ligature.4
The diagram below indicates (with blue and green) that the 'dvir' package, thanks to its dependence on TeX, can work with (very) rich text input that is properly typeset in terms of precisely placed glyphs. However, the 'dvir' package is hamstrung by having to convert from typeset glyph information back to text in order to work with the R graphics system.
The 'dvir' package suffers from several other serious limitations, including: the dependence on a (substantial) TeX installation; the fact that the user has to write LaTeX to describe the rich text input; and the fact that 'dvir' only works with some graphics devices (e.g., the Cairo-based graphics devices).
The existing packages described above show that a lot of the necessary pieces that are required to draw more sophisticated text in R exist in one form or another. What is required in order to bring all of the pieces together?
The simple summary is that we cannot do everything we want to because the R graphics engine does not allow rich text to pass through to graphics devices and the R graphics engine does not allow typeset glyph information to pass through to graphics devices.
The obvious solutions are: allow rich text to pass through to graphics devices and/or allow typeset glyphs to pass through to graphics devices. Allowing rich text requires several issues to be resolved: What sort of rich text should be allowed and how will that be passed through? Should there be a single rich text format or should we allow multiple formats (with possibly large implications for graphics devices)? At this stage, there is no strong consensus on these issues, so we have just focused on the second part of the problem: allowing typeset glyphs to be passed through. This allows multiple rich text formats and multiple typesetting engines to be accommodated and it is a much simpler task to decide on a single structure for passing information about typeset glyphs from the R graphics engine to graphics devices.
The following section describes the new interface in R graphics that allows typeset glyph information to be passed to the graphics engine, which then passes it on to graphics devices for rendering. The Section on Generating typeset glyphs describes some early experiments with packages for generating typeset glyph information that can take advantage of this new graphics interface.
This section describes the new user interface and the new graphics device interface that allows typeset glyph information to be passed through to graphics devices.
The first step is to describe a set of typeset glyphs.
This is achieved via the grDevices::glyphInfo()
function,
along with additional support from
the grDevices::glyphFont()
function (at least).
glyphInfo(id, x, y, font, size, fontList, width, height, hAnchor, vAnchor, col=NA)
The id
argument is a vector of glyph identifiers.
These are numeric indices into the list of glyphs within a font;
rather than specifying text to draw, we specify individual glyphs.
For example, the following ids
specify the
70th, 434th, 559th, etc glyphs within a font (though not
necessarily the same font).
What those ids
mean will depend entirely on the
fonts that we use.
There may be repetitions of some glyphs, e.g., the id
559
occurs several times, but only if those glyph identifiers refer to
the same font.
ids <- c(70, 434, 559, 559, 391, 559, 475, 1642, 37, 475, 510, 521)
The x
and y
arguments to glyphInfo()
provide the locations at which
to draw the glyphs. These locations are in points (1/72 inches).
For example, the following locations describe a horizontal, left-to-right
placement of the glyphs (the x
values increase monotonically
and the y
values are constant).
x <- c(0, 7.5, 15.03125, 20.140625, 25.203125, 33.484375, 38.59375, 42.1875, 45.421875, 55.328125, 58.671875, 66.84375) y <- rep(0, 12)
The font
argument to glyphInfo()
describes the font to use, though this is only an integer
index into the fontList
argument,
which contains the real font information
(see below).
The size
argument to glyphInfo()
is the size
at which to draw the glyphs (in points).
For example, the following font indices and sizes specify that the
first seven glyphs all come
from the same font (the first font in the
fontList
) and the remaining five glyphs all come
from a different font (the second font in the fontList
).
The glyphs are all at the same font size (12pt).
fonts <- rep(1:2, c(7, 5)) size <- rep(12, 12)
The fontList
argument to glyphInfo()
is a list of fonts (that the font
argument
refers to). Each component of the list should be constructed
using the glyphFont()
function and the overall
list of fonts should be constructed using
glyphFontList()
.
glyphFont(file, index, family, weight, style, PSname=NA) glyphFontList(...)
The glyphFont()
function specifies a font in precise detail via
a font file
, an integer index
(if the font file contains more than one font), plus
a family
font name,
a numeric weight
, and a character style
.
For example, the following code describes two versions of
the Google font "Montserrat", one bold and italic and one plain.
These fonts are distributed as part of the 'grDevices' package.
font1 <- glyphFont(system.file("fonts", "Montserrat", "static", "Montserrat-BoldItalic.ttf", package="grDevices"), 0, "Montserrat", 700, "italic") font2 <- glyphFont(system.file("fonts", "Montserrat", "static", "Montserrat-Medium.ttf", package="grDevices"), 0, "Montserrat", 400, "normal")
Going back to glyphInfo()
, the width
and
height
arguments describe a bounding box around the
glyphs. These will become more relevant later, as will the
hAnchor
and vAnchor
arguments, which
can be left missing in simple cases
(see the Section on Integrating typeset glyphs).
The following width and height provide a bounding box for the
glyphs in our example.
width <- 74.46875 height <- 13.11719
Finally, the col
argument provides a colour for
each glyph. A value of NA
means that the colour
can be set at drawing time
(see the Section on Integrating typeset glyphs).
In this case, we specify that the first seven glyphs should
be drawn in racing red.
col <- rep(c(rossoCorsa, NA), c(7, 5))
We now have all that we need to create a complete set of glyph information.
The following code describes a set of twelve glyphs from two
Montserrat
fonts, with the glyphs typeset horizontally from
left-to-right.
glyphs <- glyphInfo(ids, x, y, fonts, size, glyphFontList(font1, font2), width, height, col=col)
All we need to do now is draw the glyphs. This is achieved using the
grid::grid.glyph()
function
(there is no interface for the 'graphics' package at this stage).
grid.glyph(glyphInfo, x = .5, y = .5, default.units = "npc", hjust = "centre", vjust = "centre", gp = gpar(), vp = NULL, name = NULL)
The only required argument to
grid::grid.glyph()
is the glyphInfo
, as produced by the glyphInfo()
function. The glyphs will be drawn
by default in the centre of the current 'grid' viewport.
For example, the following code draws the set of glyphs that we
constructed above. We can now finally see that the glyphs
that we have typeset spell out "Ferrari Dino".
grid.glyph(glyphs)
The new user interface for rendering typeset glyphs is mirrored
in a new interface between the R graphics engine and graphics
devices.
This interface is slightly simpler because it only deals with
a set of glyphs that have a common font
,
size
, colour
, and
rot
ation.
The font
argument is an R object, with
C-level accessors provided, such as
R_GE_glyphFontFile()
.
dev->glyph(int n, int *glyphs, double *x, double *y, SEXP font, double size, int colour, double rot, pDevDesc dd);
The diagram below indicates (with
green)
that the R graphics engine can now pass typeset glyph information
through to graphics devices.
So far, support for this
feature has been added to Cairo-based devices, the pdf()
device, the quartz()
device, and the devices
provided by the 'ragg' package (in an
experimental fork).
The previous section described the information that can now be provided to R in order to draw typeset glyphs. We now turn to the problem of generating that typeset glyph information. How do we generate sensible glyph indices, x/y locations, and detailed font descriptions? This is the job of a "typesetting engine". R itself does not offer a typesetting engine at this stage, so this is an area where third party packages can be developed. This section looks at two packages that can already be used to provide us with typeset glyph information.
The 'textshaping' package was mentioned previously in the context of
providing typesetting support for the 'ragg' package.
The 'ragg' package only makes use of a subset of the typesetting
capabilities of the 'textshaping' package because 'ragg' is only ever
sent plain text from the graphics engine, so it can only
send plain text on to the 'textshaping' package.
However, 'textshaping' also provides an R-level interface
for typesetting text via the shape_text()
function.
This interface allows us to submit a limited form of rich text, consisting of multiple text items, each with its own style (font information), plus overall layout information such as a maximum width and paragraph indenting.
The result of a call to shape_text()
is a set of typeset glyph information.
For example, the following code typesets the word
"Ferrari" and the word "Dino" using two 12pt Montserrat fonts.
library(textshaping)
shape_text(c("Ferrari", " Dino"), id=1, size=12, path=c(system.file("fonts", "Montserrat", "static", "Montserrat-BoldItalic.ttf", package="grDevices"), system.file("fonts", "Montserrat", "static", "Montserrat-Medium.ttf", package="grDevices")), index=0)
$shape glyph index metric_id string_id x_offset y_offset x_midpoint 1 0 70 1 1 0.00000 0 3.750000 2 1 434 1 1 7.50000 0 3.765625 3 2 559 1 1 15.03125 0 2.546875 4 3 559 1 1 20.14062 0 2.531250 5 4 391 1 1 25.20312 0 4.140625 6 5 559 1 1 33.48438 0 2.546875 7 6 475 1 1 38.59375 0 1.796875 8 0 1642 1 2 42.18750 0 1.609375 9 1 37 1 2 45.42188 0 4.953125 10 2 475 1 2 55.32812 0 1.671875 11 3 510 1 2 58.67188 0 4.078125 12 4 521 1 2 66.84375 0 3.812500 $metrics string width height left_bearing right_bearing top_bearing bottom_bearing left_border 1 Ferrari Dino 74.46875 26.23438 0.34375 0.515625 2.171875 2.921875 0 top_border pen_x pen_y 1 11.60938 74.46875 0
As demonstrated in the previous section, the result above provides
all of the information that we need to feed the new
typeset glyph interface, glyphInfo()
and then grid.glyph()
, in order to render this result.
The shape_text()
function does not (yet) provide access
to all of the capabilities of the 'textshaping' package. See the
"Modern Text Features in R" blog post for more details
and examples.
The diagram below indicates (with blue and green) that we can now use 'textshaping' to input rich text (in a limited format) and produce typeset glyph information. The typeset glyphs can then pass through the graphics engine to graphics devices, both those internal to R and third-party devices.
This is the workflow that was used to layout the text quote from Hiroshi Tabata within the bar plot of Japanese immigration data in the Introduction.
The 'xdvir' package (Murrell, 2023b) is an evolution of the 'dvir' package that is under active development (on github). Like 'dvir', the 'xdvir' package can read DVI files, as produced by the TeX typesetting system.
In this scenario, we can work with very rich text in the form of LaTeX code. For example, the LaTeX code below describes text with the word Ferrari in bold italic racing red and the word "Dino" in a plain font.
\documentclass{standalone} \usepackage{xcolor} \begin{document} \sffamily\definecolor{rossoCorsa}{RGB}{212,0,0}{\color{rossoCorsa}\bfseries\itshape Ferrari} Dino \end{document}
The result of typesetting that rich text is a DVI file that contains typeset glyph information; which glyphs to draw from which fonts and where to draw them. For example, the output below shows (part of) the DVI file that is produced by typesetting the LaTeX code above (using the LuaTeX engine).
xxx1 k=26 x=color push rgb 0.83138 0 0 fnt_def_1 fontnum=27, checksum=0, scale=655360, design=655360, fontname=[lmsans10-boldoblique]:+tlig; fnt_num_27 set_char_70 'F' right2 b=-20316 set_char_101 'e' set_char_114 'r' set_char_114 'r' set_char_97 'a' right2 b=-20316 set_char_114 'r' set_char_105 'i' xxx1 k=9 x=color pop right3 b=218235 fnt_def_1 fontnum=25, checksum=0, scale=655360, design=655360, fontname=[lmsans10-regular]:+tlig; fnt_num_25 set_char_68 'D' set_char_105 'i' set_char_110 'n' set_char_111 'o'
The 'xdvir' package extracts values from that DVI information
to feed into the new typeset glyph interface, glyphInfo()
,
and then on to grid.glyph()
to render the result.
The difference from the 'dvir' package
is that the typeset glyph information
from a DVI file can now be passed directly to R;
there is no longer a need to convert glyphs back into text
in order to make things digestible by the R graphics engine.
The diagram below indicates (with blue and green) that, with 'xdvir', we can make use of the TeX system to write rich text and to produce typeset glyph information. The typeset glyphs can then pass through the graphics engine to graphics devices, both those internal to R and third-party devices.
The potential value of the 'xdvir' package is very large because the TeX system can be used to perform a wide range of sophisticated typesetting tasks. For example, this workflow was used to produce the vertical Japanese text in the plot of Japanese immigration data in the Introduction. However, at the time of writing, the 'xdvir' package has only been developed to the point of demonstrating a few specific DVI files.
The examples in the Section on Rendering typeset glyphs
showed the information that is required to specify a set of
typeset glyphs.
However, that section did not address the issue of locating the
typeset glyphs within an R plot.
A call to grid.glyph()
will by default just
centre the glyphs within the current 'grid' viewport.
In this section, we look at how to control the placement
of typeset glyphs in more detail.
The grid.glyph()
function has arguments x
and y
to specify the location of the typeset glyphs
and hjust
and vjust
to specify the
justification of the typeset glyphs relative to that location.
For example, the following code draws the typeset glyphs left
justified relative to the left edge of the current viewport.
grid.glyph(glyphs, x=0, hjust="left")
The horizontal justifications "left"
,
"centre"
,
and "right"
(and "center"
)
are always available
(and "bottom"
and "top"
for
vertical justification),
but it is also possible to specify other "anchor" points.
This requires specifying the anchor points as part of the
call to glyphInfo()
and using the glyphAnchor()
function.
In order to demonstrate the idea of different anchors, the diagram below shows a detailed view of just the typeset glyphs that spell the word "Dino". In this diagram, there are four glyphs typeset horizontally; the locations at which the glyphs are drawn - the glyph "origins" - are represented by small black dots. The blue rectangles show that each glyph has a width that determines where the subsequent glyph will be placed (modulo kerning). This leads to a simple idea of "left", the origin of the first glyph, and "right", the location where the next glyph would be placed after the last glyph, with a "centre" point half way between. These provide the basis for the default horizontal justification anchor points.
However, the red rectangles in the diagram show that each glyph also has a tight bounding box around the actual rendered extent of the glyph. The "ink" used to draw the "D" glyph only begins to the right of the glyph origin and does not extend as far to the right as the width of the glyph. This is repeated to varying degrees by the three other glyphs. This leads to the idea of other possible horizontal anchor points, such as the leftmost and rightmost positions where ink is drawn, denoted by "inkleft" and "inkright" on the diagram.
In the vertical direction, we have simple bottom and top anchor points that represent font-level bounds, akin to line height, but there are also other possible anchor points, "inkbottom" and "inktop", that are based on the tight bounding boxes of the glyphs. Another very important alternative anchor point is the "baseline" of the text; this is the y location that would be used to vertically align this text with another set of (horizontally typeset) glyphs. Note that the "inkbottom" anchor is very slightly lower than the "baseline" anchor because the bottom edge of the "o" dips a tiny amount below the baseline.
By default, the "Dino" glyphs will be centred on the current 'grid' viewport. More specifically, the "centre" horizontal anchor and the "centre" vertical anchor will be aligned with the centre of the viewport (as indicated by grey lines in the image below).
grid.glyph(dinoGlyphs)
The hAnchor
and vAnchor
arguments to
glyphInfo()
allow additional anchor points to be
specified by supplying a call to
the glyphAnchor()
function and providing both a
location and a label for each anchor point.
For example, the following code snippet shows part of a call to
glyphInfo()
that
specifies multiple vertical anchors as represented in the diagram
above.
dinoGlyphs <- glyphInfo(..., vAnchor=glyphAnchor(c(fymin, vcentre, fymax, min(ymin), 0, max(ymax))*scale, label=c("bottom", "centre", "top", "inkbottom", "baseline", "inktop")), ...)
With those anchor points specified, we can align any of the anchor points with an x/y location within a 'grid' viewport. For example, the following code renders the "Dino" glyphs and aligns the "bottom" anchor with the vertical centre of the current viewport (indicated by a grey horizontal line).
grid.glyph(dinoGlyphs, y=.5, vjust="bottom")
The following code repeats the render, but this time aligns the "baseline" anchor with the centre of the viewport.
grid.glyph(dinoGlyphs, y=.5, vjust="baseline")
It is also possible to specify horizontal or vertical justification
as a numeric value. In this case, 0 corresponds to
left/bottom-justification, 0.5 to centre-justification, and
1 to right/top-justification.
For example, the following code renders the "Dino" glyphs
with vjust=0
, which is the same as
vjust="bottom"
.
grid.glyph(dinoGlyphs, y=.5, vjust=0)
However, rather than being limited to a finite set of
named anchor points,
numeric justification allows us to specify any value.
For example, the following code specifies vjust=0.4
.
What this means is that the y location for the glyphs will be aligned
with a position 0.4 of the way up the height of the glyphs.
grid.glyph(dinoGlyphs, y=.5, vjust=0.4)
Even greater flexibility is possible because, as well as being able
to define additional anchors as part of a set of typeset glyphs, we
can define additional widths and/or heights.
This information is supplied to the glyphInfo()
call,
using the glyphWidth()
and/or
glyphHeight()
function
and providing width or height values, a label for each width or height, and
a label that identifies a horizontal anchor for the left of each width
(or a vertical anchor for the bottom of each height).
For example, the following code snippet shows part of a call to
glyphInfo()
that
specifies multiple heights as represented in the diagram
above.
dinoGlyphs <- glyphInfo(..., height=glyphHeight(c(height, inkheight, max(ymax))*scale, label=c("height", "inkheight", "ascent"), bottom=c("bottom", "inkbottom", "baseline")), ...)
Given multiple heights, we can provide a numeric justification
and select the particular height that the justification is relative to
with the glyphJust()
function.
For example, the following code specifies a numeric justification of
0 relative to the "ascent" height. This is equivalent to
vjust="baseline"
.
grid.glyph(dinoGlyphs, y=.5, vjust=glyphJust(0, "ascent"))
The following code demonstrates that, as well as selecting any value between 0 and 1, it is also possible to use numeric justifications outside the 0 to 1 range. Here we are aligning the "baseline" of the typeset glyphs one full "ascent" height above the middle of the current viewport.
grid.glyph(dinoGlyphs, y=.5, vjust=glyphJust(-1, "ascent"))
Like most 'grid' functions,
the grid.glyph()
function has a gp
argument for setting graphical parameters such as colour and line type.
However, grid.glyph()
ignores most graphical parameter
settings because they have already been determined for the typeset
glyphs. In particular, fontfamily
,
fontface
, and fontsize
have no effect
on typeset glyphs.
The one exception is the col
(colour) setting.
The col
argument to glyphInfo
can
contain NA
values, in which case, the colour
of the glyphs will be taken from the gp
setting
in grid.glyph()
(or the current inherited settings).
For example, the glyphs
that we constructed
in the Section on Rendering typeset glyphs specifed the colour
rossoCorsa
for the first seven glyphs, but
NA
for the last five glyphs.
In the following code, we render those glyphs again, specifying
col="grey"
, so the last five glyphs are all drawn
grey.
grid.glyph(glyphs, gp=gpar(col="grey"))
This document describes the addition of an R graphics interface for rendering typeset glyphs. This is largely groundwork for others to build upon. There are two large problems to still resolve: an interface that allows the user to enter rich (marked up) text; and a typesetting engine that can layout the rich text to produce a set of typeset glyphs. The packages 'textshaping' and 'xdvir' that are mentioned in this document are examples of the sort of development that can now take place to bring sophisticated text layout to R.
For users and developers wanting to experiment with the new capabilities, there are several important limitations:
Support for dev->glyph()
has only been implemented
so far for Cairo-based graphics devices and the pdf()
and quartz()
devices.
A proof-of-concept has also been developed for the 'ragg' package, but only in an experimental fork.
The pdf()
support does not embed fonts.
Some PDF viewers (e.g., ghostscript; Artifex Software Inc., 2023) may cope
with this, but a more reliable result will be obtained by
embedding the fonts with the grDevices::embedGlyphs()
utility.
Support is unlikely or impossible for some graphics devices. For example, the 'svglite' graphics device (Wickham et al., 2022), due to the nature of the underlying SVG language, does not have the ability to render individual glyphs.
Glyph rendering may only work for True Type and Open Type fonts. That is certainly true for the Cairo-based devices, with Cairo graphics having dropped support for Type 1 fonts.
The device support has not been optimised at all at this stage. For example, there is no caching of loaded fonts on Cairo or Quartz devices.
R objects containing glyph information, as generated by
glyphInfo()
, include fixed file paths to font files.
This means that they are not portable across different
computer environments. This portability issue infects
glyphGrob()
objects by association.
The workflow from rich text input to rendered glyphs is expected to take place within the same R session and on a single hardware environment.
The examples and discussion in this report relate to R version 4.3.0, plus the following packages: 'dvir' version 0.4-0 (experimental "glyphs" branch); 'textshaping' version 0.3.6; 'ragg' version 1.2.5; 'gridtext' version 0.1.5; and 'xdvir' version 0.0-1.
This report was generated within a Docker container (see Resources section below).
1 This quote was made during a press conference on January 6, 2022, although that information was generated in a conversation with ChatGPT, so it is not clear that this quote is real; it could have been constructed by ChatGPT. The Japanese version of the quote was also generated by ChatGPT so there is no claim made as to its accuracy, though Google Translate suggests that it is not too far from sensible.
2 The data for this plot was taken from the Statistics Bureau of Japan via their Statistics Dashboard, which has both an English and Japanese version. The title and y-axis label were also copied from those sites so they are presumably reasonable translations of the English (and vice versa).
3 This is a Google Translate translation of "Ferrari Dino" into Hebrew. I have no ability to read or write Hebrew, so I can only apologise for what Google Translate has produced.
4 And apparently ligatures like it never will be added to UNICODE.
This work was partly funded by a donation from R Studio to the University of Auckland Foundation.
Murrell, P., Pedersen, T. L., and Urbanek, S. (2023). "Rendering Typeset Glyphs in R Graphics", Technical Report 2023-01, Department of Statistics, The University of Auckland. Version 1. [ bib | DOI | http ]
This document
by Paul
Murrell is licensed under a Creative
Commons Attribution 4.0 International License.