Text and fonts
A tale of two APIs
There are two ways to draw text in Luxor. You can use either the so-called 'toy' API or the 'pro' API.
Both have their advantages and disadvantages, and, given that trying to write anything definitive about font usage on three very different operating systems is an impossibility, trial and error will eventually lead to code patterns that work for you, if not other people.
The Toy API
Use:
text(string, [position])
to place text at a position, otherwise at0/0
, and optionally specify the horizontal and vertical alignmentfontface(fontname)
to specify the fontnamefontsize(fontsize)
to specify the fontsize in points
fontsize(18)
fontface("Georgia-Bold")
text("Georgia: a serif typeface designed in 1993 by Matthew Carter.", halign=:center)
┌ Warning: The function `cfunction` is now written as a macro `@cfunction`.
│ caller = get_stream_callback at Cairo.jl:145 [inlined]
└ @ Core Cairo.jl:145
The label()
function also uses the Toy API.
The Pro API
Use:
setfont(fontname, fontsize)
to specify the fontname and size in pointssettext(text, [position])
to place the text at a position, and optionally specify horizontal and vertical alignment, rotation (in degrees counterclockwise!), and the presence of any Pango-flavored markup.
setfont("Georgia Bold", 18)
settext("Georgia: a serif typeface designed in 1993 by Matthew Carter.", halign="center")
WARNING: Base.uninitialized is deprecated, use undef instead.
likely near /Users/travis/build/JuliaGraphics/Luxor.jl/docs/make.jl:3
WARNING: Base.uninitialized is deprecated, use undef instead.
likely near /Users/travis/build/JuliaGraphics/Luxor.jl/docs/make.jl:3
WARNING: Base.uninitialized is deprecated, use undef instead.
likely near /Users/travis/build/JuliaGraphics/Luxor.jl/docs/make.jl:3
┌ Warning: The function `cfunction` is now written as a macro `@cfunction`.
│ caller = get_stream_callback at Cairo.jl:145 [inlined]
└ @ Core Cairo.jl:145
Specifying the font ("Toy" API)
Use fontface(fontname)
to choose a font, and fontsize(n)
to set the font size in points.
Luxor.fontface
— Function.fontface(fontname)
Select a font to use. (Toy API)
Luxor.fontsize
— Function.fontsize(n)
Set the font size to n
points. The default size is 10 points. (Toy API)
Specifying the font ("Pro" API)
To select a font in the Pro text API, use setfont()
and supply both the font name and a size.
Luxor.setfont
— Function.setfont(family, fontsize)
Select a font and specify the size in points.
Example:
setfont("Helvetica", 24)
settext("Hello in Helvetica 24 using the Pro API", Point(0, 10))
Placing text ("Toy" API)
Use text()
to place text.
pt1 = Point(-100, 0)
pt2 = Point(0, 0)
pt3 = Point(100, 0)
sethue("black")
text("1", pt1, halign=:left, valign = :bottom)
text("2", pt2, halign=:center, valign = :bottom)
text("3", pt3, halign=:right, valign = :bottom)
text("4", pt1, halign=:left, valign = :top)
text("5 ", pt2, halign=:center, valign = :top)
text("6", pt3, halign=:right, valign = :top)
sethue("red")
map(p -> circle(p, 4, :fill), [pt1, pt2, pt3])
┌ Warning: The function `cfunction` is now written as a macro `@cfunction`.
│ caller = get_stream_callback at Cairo.jl:145 [inlined]
└ @ Core Cairo.jl:145
fontsize(10)
fontface("Georgia")
[text(string(theta), Point(40cos(theta), 40sin(theta)), angle=theta)
for theta in 0:pi/12:47pi/24]
┌ Warning: The function `cfunction` is now written as a macro `@cfunction`.
│ caller = get_stream_callback at Cairo.jl:145 [inlined]
└ @ Core Cairo.jl:145
Luxor.text
— Function.text(str)
text(str, pos)
text(str, pos, angle=pi/2)
text(str, x, y)
text(str, pos, halign=:left)
text(str, valign=:baseline)
text(str, valign=:baseline, halign=:left)
text(str, pos, valign=:baseline, halign=:left)
Draw the text in the string str
at x
/y
or pt
, placing the start of the string at the point. If you omit the point, it's placed at the current 0/0
. In Luxor, placing text doesn't affect the current point.
angle
specifies the rotation of the text relative to the current x-axis.
Horizontal alignment halign
can be :left
, :center
, (also :centre
) or :right
. Vertical alignment valign
can be :baseline
, :top
, :middle
, or :bottom
.
The default alignment is :left
, :baseline
.
This uses Cairo's Toy text API.
Placing text ("Pro" API)
Use settext()
to place text. You can include some pseudo-HTML markup.
rulers()
sethue("black")
settext("<span font='26' background ='green' foreground='red'> Hey</span>
<i>italic</i> <b>bold</b> <sup>superscript</sup>
<tt>monospaced</tt>",
halign="center",
markup=true,
angle=10) # degrees counterclockwise!
┌ Warning: The function `cfunction` is now written as a macro `@cfunction`.
│ caller = get_stream_callback at Cairo.jl:145 [inlined]
└ @ Core Cairo.jl:145
Luxor.settext
— Function.settext(text, pos;
halign = "left",
valign = "bottom",
angle = 0, # degrees!
markup = false)
settext(text;
kwargs)
Draw the text
at pos
(if omitted defaults to 0/0
). If no font is specified, on macOS the default font is Times Roman.
To align the text, use halign
, one of "left", "center", or "right", and valign
, one of "top", "center", or "bottom".
angle
is the rotation - in counterclockwise degrees, rather than Luxor's default clockwise (+x-axis to +y-axis) radians.
If markup
is true
, then the string can contain some HTML-style markup. Supported tags include:
<b>, <i>, <s>, <sub>, <sup>, <small>, <big>, <u>, <tt>, and <span>
The <span>
tag can contains things like this:
<span font='26' background='green' foreground='red'>unreadable text</span>
Notes on fonts
On macOS, the fontname required by the Toy API's fontface()
should be the PostScript name of a currently activated font. You can find this out using, for example, the FontBook application.
On macOS, a list of currently activated fonts can be found (after a while) with the shell command:
system_profiler SPFontsDataType
Fonts currently activated by a Font Manager can be found and used by the Toy API but not by the Pro API (at least on my macOS computer currently).
On macOS, you can obtain a list of fonts that fontconfig
considers are installed and available for use (via the Pro Text API with setfont()
) using the shell command:
fc-list | cut -f 2 -d ":"
although typically this lists only those fonts in /System/Library/Fonts
and /Library/Fonts
, and not ~/Library/Fonts
.
(There is a Julia interface to fontconfig
at Fontconfig.jl.)
In the Pro API, the default font is Times Roman (on macOS). In the Toy API, the default font is Helvetica (on macOS).
Cairo (and hence Luxor) doesn't support emoji currently. 😢
Text to paths
textpath()
converts the text into graphic paths suitable for further manipulation.
Luxor.textpath
— Function.textpath(t)
Convert the text in string t
to a new path, for subsequent filling/stroking etc...
Font dimensions ("Toy" API)
The textextents(str)
function gets an array of dimensions of the string str
, given the current font.
The green dot is the text placement point and reference point for the font, the yellow circle shows the text block's x and y bearings, and the blue dot shows the advance point where the next character should be placed.
Luxor.textextents
— Function.textextents(str)
Return an array of six Float64s containing the measurements of the string str
when set using the current font settings (Toy API):
1 x_bearing
2 y_bearing
3 width
4 height
5 x_advance
6 y_advance
The x and y bearings are the displacement from the reference point to the upper-left corner of the bounding box. It is often zero or a small positive value for x displacement, but can be negative x for characters like "j"; it's almost always a negative value for y displacement.
The width and height then describe the size of the bounding box. The advance takes you to the suggested reference point for the next letter. Note that bounding boxes for subsequent blocks of text can overlap if the bearing is negative, or the advance is smaller than the width would suggest.
Example:
textextents("R")
returns
[1.18652; -9.68335; 8.04199; 9.68335; 9.74927; 0.0]
Labels
The label()
function places text relative to a specific point, and you can use compass points to indicate where it should be. So :N
(for North) places a text label directly above the point.
sethue("black")
fontsize(15)
octagon = ngon(O, 100, 8, 0, vertices=true)
compass = [:SE, :S, :SW, :W, :NW, :N, :NE, :E, :SE]
for i in 1:8
circle(octagon[i], 5, :fill)
label(string(compass[i]), compass[i], octagon[i], leader=true, leaderoffsets=[0.2, 0.9], offset=50)
end
┌ Warning: The function `cfunction` is now written as a macro `@cfunction`.
│ caller = get_stream_callback at Cairo.jl:145 [inlined]
└ @ Core Cairo.jl:145
Luxor.label
— Function.label(txt::String, alignment::Symbol=:N, pos::Point=O;
offset=5,
leader=false)
Add a text label at a point, positioned relative to that point, for example, :N
signifies North and places the text directly above that point.
Use one of :N
, :S
, :E
, :W
, :NE
, :SE
, :SW
, :NW
to position the label relative to that point.
label("text") # positions text at North (above), relative to the origin
label("text", :S) # positions text at South (below), relative to the origin
label("text", :S, pt) # positions text South of pt
label("text", :N, pt, offset=20) # positions text North of pt, offset by 20
The default offset is 5 units.
If leader
is true, draw a line as well.
TODO: Negative offsets don't give good results.
label(txt::String, rotation::Float64, pos::Point=O;
offset=5,
leader=false,
leaderoffsets=[0.0, 1.0])
Add a text label at a point, positioned relative to that point, for example, 0.0
is East, pi
is West.
label("text", pi) # positions text to the left of the origin
Text on a curve
Use textcurve(str)
to draw a string str
on a circular arc or spiral.
using Luxor
Drawing(1800, 1800, "/tmp/text-spiral.png")
origin()
background("ivory")
fontsize(18)
fontface("LucidaSansUnicode")
sethue("royalblue4")
textstring = join(names(Base), " ")
textcurve("this spiral contains every word in julia names(Base): " * textstring,
-pi/2,
800, 0, 0,
spiral_in_out_shift = -18.0,
letter_spacing = 0,
spiral_ring_step = 0)
fontsize(35)
fontface("Agenda-Black")
textcentered("julia names(Base)", 0, 0)
finish()
preview()
For shorter strings, textcurvecentered()
tries to place the text on a circular arc by its center point.
fontface("Arial-Black")
circle(O, 100, :stroke)
textcurvecentered("hello world", -pi/2, 100, O;
clockwise = true,
letter_spacing = 0,
baselineshift = -20
)
textcurvecentered("hello world", pi/2, 100, O;
clockwise = false,
letter_spacing = 0,
baselineshift = 10
)
┌ Warning: The function `cfunction` is now written as a macro `@cfunction`.
│ caller = get_stream_callback at Cairo.jl:145 [inlined]
└ @ Core Cairo.jl:145
Luxor.textcurve
— Function.Place a string of text on a curve. It can spiral in or out.
textcurve(the_text, start_angle, start_radius, x_pos = 0, y_pos = 0;
# optional keyword arguments:
spiral_ring_step = 0, # step out or in by this amount
letter_spacing = 0, # tracking/space between chars, tighter is (-), looser is (+)
spiral_in_out_shift = 0, # + values go outwards, - values spiral inwards
clockwise = true
)
start_angle
is relative to +ve x-axis, arc/circle is centered on (x_pos,y_pos)
with radius start_radius
.
Luxor.textcurvecentered
— Function.textcurvecentered(the_text, the_angle, the_radius, center::Point;
clockwise = true,
letter_spacing = 0,
baselineshift = 0
This version of the textcurve()
function is designed for shorter text strings that need positioning around a circle. (A cheesy effect much beloved of hipster brands and retronauts.)
letter_spacing
adjusts the tracking/space between chars, tighter is (-), looser is (+)). baselineshift
moves the text up or down away from the baseline.
textcurvecentred (UK spelling) is a synonym
Text clipping
You can use newly-created text paths as a clipping region - here the text paths are filled with names of randomly chosen Julia functions:
using Luxor
currentwidth = 1250 # pts
currentheight = 800 # pts
Drawing(currentwidth, currentheight, "/tmp/text-path-clipping.png")
origin()
background("darkslategray3")
fontsize(600) # big fontsize to use for clipping
fontface("Agenda-Black")
str = "julia" # string to be clipped
w, h = textextents(str)[3:4] # get width and height
translate(-(currentwidth/2) + 50, -(currentheight/2) + h)
textpath(str) # make text into a path
setline(3)
setcolor("black")
fillpreserve() # fill but keep
clip() # and use for clipping region
fontface("Monaco")
fontsize(10)
namelist = map(x->string(x), names(Base)) # get list of function names in Base.
x = -20
y = -h
while y < currentheight
sethue(rand(7:10)/10, rand(7:10)/10, rand(7:10)/10)
s = namelist[rand(1:end)]
text(s, x, y)
se = textextents(s)
x += se[5] # move to the right
if x > w
x = -20 # next row
y += 10
end
end
finish()
preview()
Text blocks, boxes, and wrapping
Longer lines of text can be made to wrap inside an imaginary rectangle with textwrap()
. Specify the required width of the rectangle, and the location of the top left corner.
fontface("Georgia")
loremipsum = """Lorem ipsum dolor sit amet, consectetur
adipiscing elit. Nunc placerat lorem ullamcorper,
sagittis massa et, elementum dui. Sed dictum ipsum vel
commodo pellentesque. Aliquam erat volutpat. Nam est
dolor, vulputate a molestie aliquet, rutrum quis lectus.
Sed lectus mauris, tristique et tempor id, accumsan
pharetra lacus. Donec quam magna, accumsan a quam
quis, mattis hendrerit nunc. Nullam vehicula leo ac
leo tristique, a condimentum tortor faucibus."""
setdash("dot")
box(O, 200, 200, :stroke)
textwrap(loremipsum, 200, O - (200/2, 200/2))
WARNING: Base.uninitialized is deprecated, use undef instead.
likely near /Users/travis/build/JuliaGraphics/Luxor.jl/docs/make.jl:3
WARNING: Base.uninitialized is deprecated, use undef instead.
likely near /Users/travis/build/JuliaGraphics/Luxor.jl/docs/make.jl:3
WARNING: Base.uninitialized is deprecated, use undef instead.
likely near /Users/travis/build/JuliaGraphics/Luxor.jl/docs/make.jl:3
WARNING: Base.uninitialized is deprecated, use undef instead.
likely near /Users/travis/build/JuliaGraphics/Luxor.jl/docs/make.jl:3
WARNING: Base.uninitialized is deprecated, use undef instead.
likely near /Users/travis/build/JuliaGraphics/Luxor.jl/docs/make.jl:3
WARNING: Base.uninitialized is deprecated, use undef instead.
likely near /Users/travis/build/JuliaGraphics/Luxor.jl/docs/make.jl:3
WARNING: Base.uninitialized is deprecated, use undef instead.
likely near /Users/travis/build/JuliaGraphics/Luxor.jl/docs/make.jl:3
WARNING: Base.uninitialized is deprecated, use undef instead.
likely near /Users/travis/build/JuliaGraphics/Luxor.jl/docs/make.jl:3
WARNING: Base.uninitialized is deprecated, use undef instead.
likely near /Users/travis/build/JuliaGraphics/Luxor.jl/docs/make.jl:3
WARNING: Base.uninitialized is deprecated, use undef instead.
likely near /Users/travis/build/JuliaGraphics/Luxor.jl/docs/make.jl:3
WARNING: Base.uninitialized is deprecated, use undef instead.
likely near /Users/travis/build/JuliaGraphics/Luxor.jl/docs/make.jl:3
WARNING: Base.uninitialized is deprecated, use undef instead.
likely near /Users/travis/build/JuliaGraphics/Luxor.jl/docs/make.jl:3
┌ Warning: The function `cfunction` is now written as a macro `@cfunction`.
│ caller = get_stream_callback at Cairo.jl:145 [inlined]
└ @ Core Cairo.jl:145
textwrap()
accepts a function that allows you to insert code that responds to the next line's linenumber, contents, position, and height.
fontface("Georgia")
loremipsum = """Lorem ipsum dolor sit amet, consectetur
adipiscing elit. Nunc placerat lorem ullamcorper,
sagittis massa et, elementum dui. Sed dictum ipsum vel
commodo pellentesque. Aliquam erat volutpat. Nam est
dolor, vulputate a molestie aliquet, rutrum quis lectus.
Sed lectus mauris, tristique et tempor id, accumsan
pharetra lacus. Donec quam magna, accumsan a quam
quis, mattis hendrerit nunc. Nullam vehicula leo ac
leo tristique, a condimentum tortor faucibus."""
textwrap(loremipsum, 200, O - (200/2, 200/2),
(lnumber, str, pt, h) -> begin
sethue(Colors.HSB(rescale(lnumber, 1, 15, 0, 360), 1, 1))
text(string("line ", lnumber), pt - (50, 0))
end)
┌ Warning: The function `cfunction` is now written as a macro `@cfunction`.
│ caller = get_stream_callback at Cairo.jl:145 [inlined]
└ @ Core Cairo.jl:145
The textbox()
function also draws text inside a box, but doesn't alter the lines, and doesn't force the text to a specific width. Supply an array of strings and the top left position. The leading
argument specifies the distance between the lines, so should be set relative to the current font size (as set with fontsize()
).
This example counts the number of characters drawn, using a simple closure.
fontface("Georgia")
fontsize(30)
loremipsum = """Lorem ipsum dolor sit amet, consectetur
adipiscing elit. Nunc placerat lorem ullamcorper,
sagittis massa et, elementum dui. Sed dictum ipsum vel
commodo pellentesque. Aliquam erat volutpat. Nam est
dolor, vulputate a molestie aliquet, rutrum quis lectus.
Sed lectus mauris, tristique et tempor id, accumsan
pharetra lacus. Donec quam magna, accumsan a quam
quis, mattis hendrerit nunc. Nullam vehicula leo ac
leo tristique, a condimentum tortor faucibus."""
_counter() = (a = 0; (n) -> a += n)
counter = _counter()
translate(Point(-600/2, -300/2) + (50, 50))
fontface("Georgia")
fontsize(20)
textbox(filter(!isempty, split(loremipsum, "\n")),
O,
leading = 28,
linefunc = (lnumber, str, pt, h) -> begin
text(string(lnumber), pt - (30, 0))
counter(length(str))
end)
fontsize(10)
text(string(counter(0), " characters"), O + (0, 230))
┌ Warning: The function `cfunction` is now written as a macro `@cfunction`.
│ caller = get_stream_callback at Cairo.jl:145 [inlined]
└ @ Core Cairo.jl:145
Luxor.textwrap
— Function.textwrap(s::T where T<:AbstractString, width::Real, pos::Point;
rightgutter=5,
leading=0)
textwrap(s::T where T<:AbstractString, width::Real, pos::Point, linefunc::Function;
rightgutter=5,
leading=0)
Draw the string in s
by splitting it at whitespace characters into lines, so that each line is no longer than width
units. The text starts at pos
such that the first line of text is drawn entirely below a line drawn horizontally through that position. Each line is aligned on the left side, below pos
.
See also textbox()
.
If you don't supply a value for leading
, the font's built-in extents are used.
Text with no whitespace characters won't wrap. You can write a simple chunking function to split a string or array into chunks:
chunk(x, n) = [x[i:min(i+n-1,length(x))] for i in 1:n:length(x)]
Luxor.textbox
— Function.textbox(lines::Array, pos::Point=O;
leading = 12,
linefunc::Function = (linenumber, linetext, startpos, height) -> (),
alignment=:left)
Draw the strings in the array lines
vertically downwards. leading
controls the spacing between each line (default 12), and alignment
determines the horizontal alignment (default :left
).
Optionally, before each line, execute the function linefunc(linenumber, linetext, startpos, height)
.
See also textwrap()
, which modifies the text so that the lines fit into a specified width.
Luxor.splittext
— Function.splittext(s)
Split the text in string s
into an array, but keep all the separators attached to the preceding word.