Rethinking the spacing detection: Dual Width => Multiple width
Introduction
The current int FcFreeTypeSpacing(FT_Face face)
implementation in pseudocode:
Advances is an empty set.
for each char C in the font face
Advances.put(C.advance) // It ensures advances are unique comparing by fc_approximately_equal.
end for
case Advances.count is 0, 1
return FC_MONO
case Advances.count is 2
if Advances[0] * 2 == Advances[1] // fc_approximately_equal
return FC_DUAL
else
return FC_PROPORTIONAL
end if
case Advances.count > 2
return FC_PROPORTIONAL
Note:
- Zero-width chars are excluded;
- "fc_approximately_equal" for widths: x and y are identical, iif
|x-y| < 0.03 * max(|x|, |y|)
.
Background: Monospace and Dual Width
The word "monospace" tells user that the characters only have one width so it will have two good properties:
- User's opinion: all words and characters are aligned naturally on a visual column grid, it's good for coding and designing ASCII artworks, displaying on text terminal, etc.;
- Programmer's opinion: strictly
line_width = char_num * char_width
, so we may also calculate the max column of a row by one simple division, instead of summing all the widths up and checking if it overflowed.
To CJK, all Chinese characters are in fixed-size rectangle cell (almost a square), CJK "monospace" fonts are actually "dual-width" that generally the small width is for European characters and the doubled width is for Chinese characters, Kana and Hangul, etc.. We named its spacing FC_DUAL. It lost the property 2, but it still has the property 1 because it is still well-aligned.
From Dual Width to Multiple Width, Why and How
Rationale: There are many "big character" in Unicode that font designers may want it to be wider than a normal one. For a "monospace" font face, some font designers may want to, while not breaking the alignment, make them to be two, three or even four times the width. Here are some examples:
U+2030 ‰ Per Mille Sign
U+2031 ‱ Per Ten Thousand Sign
U+203B ※ Reference Mark
Using multiple widths (e.g. {600, 1200, 1800} in Noto Sans Mono), these characters and CJK share the same motivation and have very similar visual effect.
Therefore, I propose to add FC_MULTIPLE and FC_DUAL will be an alias to FC_MULTIPLE. This will maintain a good backward compatibility that:
- FC_DUAL fonts will be FC_DUAL/FC_MULTIPLE, old FC_DUAL is a strict subset of the new FC_MULTIPLE/FC_DUAL definition;
- FC_MONO will be also FC_MONO, we should keep this rule unchanged.
- FC_DUAL will be an alias to FC_MULTIPLE, so no new number need to be assigned, both source code and binaries are compatible.
The algorithm need to be changed, here is my version for your reference:
const LIMIT_TEMP_FACTORS = 5
Advances is an empty set.
Is_Mono = true
First_Advance = (first char).advance
Advances.put(First_Advance)
for each char C in font face
A = C.advance
if Is_Mono
if A == First_Advance
skip to next char. // The only path for monospace fonts
else
Is_Mono = false
endif
end if
if A can be divided by any element in Advances
(no-op) // The hot path for multiple width fonts
else if at least one element in Advances can be divided by A
replace the first by A, delete the others. // E.g. 1800s and 1200s come first, and then you get a 600.
else
Advances.put(A)
if Advances.count > LIMIT_TEMP_FACTORS
return FC_PROPORTIONAL // The short path for proportional spacing fonts
end if
end if
end for
if Is_Mono
return FC_MONO
end if
if Advances.count == 1
return FC_MULTIPLE // like [1200, 1800, 600] => {600}
else
return FC_PROPORTIONAL // like [1200, 1800, 1000, 600] => {1000, 600}
end if