HarfPy is a Python 3 binding for
[HarfBuzz](https://www.freedesktop.org/wiki/Software/HarfBuzz/). It is meant to be used in
conjunction with my Python wrappers for other major parts of the Linux typography stack:
* Qahirah ([GitLab](https://gitlab.com/ldo/qahirah),
[GitHub](https://github.com/ldo/qahirah)) -- my binding for the
[Cairo](https://www.cairographics.org/) graphics library
* python_freetype ([GitLab](https://gitlab.com/ldo/python_freetype),
[GitHub](https://github.com/ldo/python_freetype)) -- my binding for
[FreeType](https://www.freetype.org/)
* PyBidi ([GitLab](https://gitlab.com/ldo/pybidi),
[GitHub](https://github.com/ldo/pybidi)) -- my binding for
[FriBidi](https://fribidi.org/)
The basic steps in doing a piece of high-quality text rendering on Linux are:
* Make sure all text is encoded in Unicode. (This should go without saying...)
* Use FriBidi to reorder a string of unicode-encoded text from logical (reading) order to
visual (rendering) order. (This step can be skipped in cases where you do not mix text
of different directions.)
* Use FreeType to load a suitable font.
* Use HarfBuzz to choose and lay out suitable glyphs for the specified text, controlled by
appropriate features selected from the specified font.
* Use Cairo to draw the actual glyphs, as laid out by HarfBuzz, with that font.
Installation
============
To install HarfPy on your system, type
python3 setup.py install
This will install the Python module named “`harfbuzz`”.
Basic Usage
===========
All functionality comes from the one module:
import harfbuzz
You will typically also want more direct access to constants and other definitions within
the `HARFBUZZ` class:
from harfbuzz import \
HARFBUZZ
Or you may want to abbreviate the names, for convenience:
import harfbuzz as hb
from harfbuzz import \
HARFBUZZ as HB
Names in the module omit the “`hb_`” and "`HB_`” prefixes. For example, whereas in C you
might write
hb_shape(font, buffer);
in Python this becomes (with the above import abbreviations):
hb.shape(font, buffer)
And a constant like `HB_TAG_NONE` can be accessed (again with the above import
abbreviations) as `HB.TAG_NONE`.
Operations on specific HarfBuzz objects further lose the prefix parts of their names
associated with those objects, because they become methods defined within those objects.
Thus, instead of, in C,
buf = hb_buffer_create();
in Python this becomes
buf = hb.Buffer.create()
and
hb_buffer_clear_contents(buf);
becomes
buf.clear_contents()
Hello-HarfBuzz Example
======================
The HarfBuzz manual includes [this example](https://harfbuzz.github.io/ch03s03.html)
on how to get started. Let’s recreate that using HarfPy.
import sys
import qahirah as qah
import harfbuzz as hb
We will need to load a font with FreeType. To use FreeType, you have to create a `Library`
instance. But Qahirah already has one, so it’s simpler to just use that:
ft = qah.get_ft_lib()
(This also lets you use the same font instances for drawing the text.) Let’s make up a
simple line of right-to-left text:
text_line = "\u0627\u0644\u0642\u0627\u0647\u0631\u0629" # “al-qahirah”
Create a `Buffer`:
buf = hb.Buffer.create()
Put the text into the buffer:
buf.add_str(text_line)
Let HarfBuzz figure out the run properties:
buf.guess_segment_properties()
Load a suitable font:
ft_face = ft.find_face("Scheherazade")
We need to set a font size, otherwise we won’t get any sensible metrics. A size of 1 will
do for measurement purposes:
ft_face.set_char_size(size = 1, resolution = qah.base_dpi)
HarfBuzz wants us to wrap this in one of its own `Font` objects:
hb_font = hb.Font.ft_create(ft_face)
Do the glyph selection and layout, based on the default font features:
hb.shape(hb_font, buf)
And let’s see what we got:
sys.stdout.write \
(
"buf.glyph_infos = %s\n glyph_positions = %s\n"
%
(buf.glyph_infos, buf.glyph_positions)
)
Using OpenType Font Features
============================
An important part of OpenType is the [font features
registry](https://docs.microsoft.com/en-us/typography/opentype/spec/featurelist). This defines
standard “features” that a specific font may implement: for example, auto-kerning,
alternate glyphs for different number styles, small caps, old-style ligatures and so on.
To specify the feature settings, you just need to pass an extra argument to the `hb.shape`
method call:
hb.shape(hb_font, buf, «features»)
where «features» is a sequence of `Feature` settings to be applied. For example, to turn
on old-style ligatures over the entire line of text in the buffer:
hb.shape(hb_font, buf, [hb.Feature(HB.TAG(b'hlig'), value = 1)])
Rendering Bidirectional Text
============================
How would you render a line containing a mixture of left-to-right and right-to-left text?
First of all, note that HarfBuzz can only do shaping on one run at a time. So you have to
break up your line into segments, put them through HarfBuzz shaping one at a time, and
collect all the glyphs together. Here is an example to walk you through the necessary
steps, using all the abovementioned Python modules. First, the necessary imports:
import sys
import os
import math
# import freetype2 as freetype # use Qahirah instance
import qahirah as qah
from qahirah import \
CAIRO, \
Colour, \
Glyph, \
Vector
ft = qah.get_ft_lib()
import fribidi as fb
from fribidi import \
FRIBIDI as FB
import harfbuzz as hb
Next, the text we are going to render:
book_title = \
(
"\u0627\u0644\u0643\u062a\u0627\u0628" # “al-kitab”
" \u0627\u0644\u0645\u062e\u062a\u0635\u0631" # “al-mukhtasar”
" \u0641\u064a" # “fi”
" \u062d\u0633\u0627\u0628" # “hisab”
" \u0627\u0644\u062c\u0628\u0631" # “al-jabr”
" \u0648\u0627\u0644\u0645\u0642\u0627\u0628\u0644\u0629" # “wa’l-muqabala”
)
author = \
(
"\u0645\u062d\u0645\u062f" # “Muhammad’
" \u0628\u0646" # “ibn”
" \u0645\u0648\u0633\u0649" # “Musa”
" \u0627\u0644\u062e\u0648\u0627\u0631\u0632\u0645\u06cc" # “al-Khwarizmi”
)
text_line = \
(
"The book “%(title)s” gives us the word “algebra”,"
" its author’s name %(author)s gives us “algorithm”."
%
{"author" : author, "title" : book_title}
)
base_rtl = False # overall direction of line
Initial font and buffer setup:
text_size = 36
buf = hb.Buffer.create()
ft_face = ft.find_face("DejaVu Sans")
ft_face.set_char_size(size = text_size, resolution = qah.base_dpi)
hb_font = hb.Font.ft_create(ft_face)
Use FriBidi to reorder the line and define the embedding levels, using the
`ReorderLine` convenience wrapper class provided by PyBidi:
reordered = fribidi.ReorderLine \
(
text_line = text_line,
base_dir = (FB.PAR_LTR, FB.PAR_RTL)[base_rtl],
flags = FRIBIDI.FLAGS_DEFAULT
)
Next, collect the glyphs for each segment/run into a list of Qahirah `Glyphs` objects.
Note the `Buffer.get_glyphs` convenience method provided by HarfPy:
glyphs = []
glyph_pos = Vector(0, 0)
for substr, pos1, pos2, level in reordered.each_embedding_run(vis_order = False) :
buf.reset()
buf.add_str(substr)
buf.guess_segment_properties()
hb.shape(hb_font, buf)
new_glyphs, end_glyph_pos = buf.get_glyphs(glyph_pos)
glyph_pos = end_glyph_pos
glyphs.extend(new_glyphs)
#end for
Do the Cairo font setup, and figure out how big an `ImageSurface` we need:
qah_face = qah.FontFace.create_for_ft_face(ft_face)
glyph_extents = \
(qah.Context.create_for_dummy()
.set_font_face(qah_face)
.set_font_size(text_size)
.glyph_extents(glyphs)
)
figure_bounds = math.ceil(glyph_extents.bounds)
pix = qah.ImageSurface.create \
(
format = CAIRO.FORMAT_RGB24,
dimensions = figure_bounds.dimensions
)
Actually render the glyphs into a Cairo context:
(qah.Context.create(pix)
.translate(- figure_bounds.topleft)
.set_source_colour(Colour.grey(1))
.paint()
.set_source_colour(Colour.grey(0))
.set_font_face(qah_face)
.set_font_size(text_size)
.show_glyphs(glyphs)
)
And finally, save the `ImageSurface` contents to a PNG file for viewing:
pix.flush().write_to_png("%s.png" % os.path.basename(sys.argv[0]))
For more complete example scripts, see my harfpy_examples
([GitLab](https://gitlab.com/ldo/harfpy_examples),
[GitHub](https://github.com/ldo/harfpy_examples)) repo.
More Info About HarfBuzz
========================
The abovementioned [manual](https://harfbuzz.github.io/) has a lot of useful
reference info. Some other introductory info may be found at the
[HarfBuzz Wiki](https://github.com/behdad/harfbuzz/wiki).
Lawrence D'Oliveiro <ldo@geek-central.gen.nz>
2020 August 22