Jyutping-annotated Chinese looks dissonant. This ugliness has a deep cause. Canto Font 2.5 introduced a coarse solution, which was tuned in 2.6, and in 2.7, Jyutping-annotated Chinese has never looked better. Let me tell you how we get here.
The central conflict here is that Jyutping-annotated Chinese must contain both Chinese-Japanese-Korean (CJK) characters as well as Latin script. We expect printed CJK characters to be fixed width. This expectation is strong for poetry and lyrics, but we are also conditioned for other printed text. Use the slider in the following picture to compare characters with fixed widths and a copy with ever so slightly off spacing (10%).


Our eyes are surprisingly sensitive to these, and the changing rhythm makes reading more effortful. Design is about usability. (Of course, it’s also ugly.)
Latin scripts, on the other hand, are irregular both on the character level and on the word level. The four-character sequence iiii takes up considerably less space than mmmm. This is in turn much shorter than the seven-characters sequence of mmmmmmm. Jyutping uses Latin script, and their widths spans the very short to the very long:
- m4
- sai1
- man2
- maan3
- ngaang6
- gau2 gung1
Jyutping-annotated Chinese require us to put together fixed-width CJK ideographs with fluid-width Latin phonetic. The mismatch is fundamental.
Possible solutions
Before talking about solutions, let’s refine our understanding of the problem with some measurements. I don’t expect you to be a font-maker, so we’ll stay general with round numbers and use a “block” as our unit. (Font makers obsess over 1% differences.)
Let’s assume a CJK character to be 3 blocks wide (學); then in general a Latin alphanumeric would be somewhere between 1 (the i and 1) to 3 (the m) blocks wide. The next picture shows this using Fira Sans, and Chiron Hei, both used in the Regular Canto Font.

The first class of solution is to enforce the equal-width nature of CJK characters as sacrosanct. This means, naively, we fix the longest possible Jyutping into the width of one character:

The width of the Jyutping must be 3/22 = 14% that of the CJK character. If we use a reasonably large font size for the CJK character, like 32 points, the Latin would be a mere 4 pt. Font size does not tell us the most important part of the story, surface area does. The surface area that each Latin charcter occupies is (0.14)2 = 2% 😲 that of the CJK glyph: that’s why it’s unreadable.
An amendment would be to lock this to the longest mono-syllabic Jyutping (which would be ngaang6); that gives us about 20% width (6 pt; 4% surface area), at the expense of every bi-syllabic characters clashing into the neighbour. Those show up rare enough, so this was my choice for Canto Font 1. The general complaint is that the Jyutping is too small. I agree, but I didn’t know better and couldn’t do better.1
The second class of solutions is to prioritize what the Jyutping needs. We pick a readable size, say, the Latin being 50% width of CJK, and let the width of the overall glyph floats. The surface area is far more favorable: at (0.50)2 = 25%, the surface area covered is more than ten times larger than solution 1. However, we simultaneously stumble into the exact problem we’ve started this article with, but now unequal spacing is a real problem instead of an artificial one:

Uneven spacing is an especially bad look for poems and lyrics, where constant width of each phrase is part of the form, but it’s not really good for any case. We can alleviate this by making the Jyutping font-size smaller, but smaller Jyutping is less readable.

Most Jyutping-annotated Chinese text uses one of the above. Actually, if we take a step back and think clearly, solution 1 and 2 exists on a continuum:

When we think inside this line of thought, all adjustments are made with the same trade-offs. There is no good solution.
The Fluid-Fixed Solution
This bothered me for years. In Canto Font 2.5 I explored an alternative solution. This solution has two parts: if we fix the width of the CJK characters at wider than its true width then we preserve the equal spacing while giving more room for the Jyutping to scale. Instead of scaling to 3/16 (18% width, 3.2% area), we could now go up to 5/16 (31% width; 9.6% area). In practice a 300% increase in surface area makes this much more readable.

We can do even better when we allow the wider Jyutping to take up more space.

The only problem is that I didn’t know how to do this in a font. 2.5 solved this by establishing a 1.8 character fixed width (very wide), then splitting the “too long” glyphs into three bins: medium-wide, wide, and super-wide. Together with the fixed (narrow) width, we now have four groups of glyphs, and 4 x 4 = 16 ways two character groups can be combined. Each of these pair was assigned to a kerning group according to the widest width within that group.

This works! But not perfectly, for two reasons. The first is that there is too much space between characters, which breaks the rhythm as the reader scans across the text. (I dog-food and benchmarks the Canto Fonts.) The second is that the grouping is too coarse. A glyph that is just a touch too wide gets placed one bin higher, and this leaves too much space. It can and thus must get better. (OK, most people won’t care, but I do.)
To explore the less-bad spacing, I used the CSS `letter-spacing` parameter to interactively adjust the spacing. 1.3–1.4 looks best to my eye. But that has a problem:

The above histogram shows the distribution of character widths in the Regular Canto Font (where the Latin is 50% size of the CJK). There exists a tall bar (at 488) because most Jyutping are in fact narrower. At 1.3 character spacing, every glyph above 630 needs to float, and that’s too many. At the end of the day, we trade
- readability of tighter inter-CJK spacing, against
- readability of a regular rhythm
I balanced this at 1.55 character width, which covers about 85% of all glyphs.
Parallel with this effort, I figured out how to skip the kerning groups and directly give every too-wide characters exactly the space it needs. Putting the know-what and the know-how together, we get Jyutping that looks good up-close and from a distance:


In prose this looks even better. This is the same passage as before and I think you’d agree that this is far more pleasant to read.

If we again take a step back, we’ve added a new parameter to how Jyutping is set. Previously, where the overhang start is chained to the font-size of the Jyutping. By thinking out of line, the solution-space has expanded (into a box). Working with 5% increments, previously there were 20 options to try; now there are 20 x 20 = 400, and of the new-380, some are better than the best of the old-20.

In hindsight this is a simple, natural solution that should not have taken years (!) and half-a-million SVGs (!!) to get right. What happened is that implementing the solution requires being able to access and control the metrics of every single glyph. Ideas are not very useful until they can be made real.
Recipe / tl;dr
Cantonese with Jyutping can look good. Looking good requires balancing:
- surface area ratios of Jyutping and CJK characters, and
- preserving even distance between CJK characters.
You can optimize this by:
- choosing a tall x-height Latin font. (This is why I used Fira Sans!)
- set the Jyutping size to 40-60%.
- locking the inter-character spacing at around 1.4-1.7. (This is on top of about 4+4% side-bearings.)
- floating the width if the Jyutping is too wide.
Or, if you are not a glutton for pain, just use the Cantonese Fonts 🙂
Postscript
I haven’t investigated how this would be done in HTML layout, a typesetting system like LaTeX / Typst, much less in InDesign / Affinity. Strange enough then, the Canto Fonts are the only place I can access this too.
My intuition tells me that there is a different optimum for combinations of CJK font-family, Jyutping font-family, and type of text. Building color CJK font is tedious and not a rapid exploratory method. Future work here require building out a flexible LaTeX/Typst/HTML workflow, so both dimensions of the solution-space can be programmatically enumerated.
- Canto Font 1 does contextual pronunciation with pre-rendered SVGs for words, which came from LaTeX. The spacing between font characters is controlled by metrics in the font, whereas spacing between characters in word SVGs are controlled by LaTeX. They must be identical and I’m not sure it is possible to have cohesive rules that works the same way in both. I certainly never felt precise controls of LaTeX ruby text. ↩︎



Leave a Reply
You must be logged in to post a comment.