Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 1e6946c

Browse files
authored
Merge pull request #14940 from QuLogic/fix-kerning
Fix text kerning calculations and some FT2Font cleanup
2 parents c634e63 + f10da0c commit 1e6946c

19 files changed

+113
-14
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
Kerning adjustments now use correct values
2+
------------------------------------------
3+
4+
Due to an error in how kerning adjustments were applied, previous versions of
5+
Matplotlib would under-correct kerning. This version will now correctly apply
6+
kerning (for fonts supported by FreeType). To restore the old behavior (e.g.,
7+
for test images), you may set :rc:`text.kerning_factor` to 6 (instead of 0).
8+
Other values have undefined behavior.
9+
10+
.. plot::
11+
12+
import matplotlib.pyplot as plt
13+
14+
# Use old kerning values:
15+
plt.rcParams['text.kerning_factor'] = 6
16+
fig, ax = plt.subplots()
17+
ax.text(0.0, 0.05, 'BRAVO\nAWKWARD\nVAT\nW.Test', fontsize=56)
18+
ax.set_title('Before (text.kerning_factor = 6)')
19+
20+
Note how the spacing between characters is uniform between their bounding boxes
21+
(above). With corrected kerning (below), slanted characters (e.g., AV or VA)
22+
will be spaced closer together, as well as various other character pairs,
23+
depending on font support (e.g., T and e, or the period after the W).
24+
25+
.. plot::
26+
27+
import matplotlib.pyplot as plt
28+
29+
# Use new kerning values:
30+
plt.rcParams['text.kerning_factor'] = 0
31+
fig, ax = plt.subplots()
32+
ax.text(0.0, 0.05, 'BRAVO\nAWKWARD\nVAT\nW.Test', fontsize=56)
33+
ax.set_title('After (text.kerning_factor = 0)')

examples/misc/font_indexing.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,4 @@
3535
print('AV', font.get_kerning(glyphd['A'], glyphd['V'], KERNING_DEFAULT))
3636
print('AV', font.get_kerning(glyphd['A'], glyphd['V'], KERNING_UNFITTED))
3737
print('AV', font.get_kerning(glyphd['A'], glyphd['V'], KERNING_UNSCALED))
38-
print('AV', font.get_kerning(glyphd['A'], glyphd['T'], KERNING_UNSCALED))
38+
print('AT', font.get_kerning(glyphd['A'], glyphd['T'], KERNING_UNSCALED))

lib/matplotlib/font_manager.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1338,7 +1338,8 @@ def is_opentype_cff_font(filename):
13381338
def get_font(filename, hinting_factor=None):
13391339
if hinting_factor is None:
13401340
hinting_factor = rcParams['text.hinting_factor']
1341-
return _get_font(os.fspath(filename), hinting_factor)
1341+
return _get_font(os.fspath(filename), hinting_factor,
1342+
_kerning_factor=rcParams['text.kerning_factor'])
13421343

13431344

13441345
def _rebuild():
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
# This patch should go on top of the "classic" style and exists solely to avoid
22
# changing baseline images.
33

4+
text.kerning_factor : 6
5+
46
ytick.alignment: center_baseline

lib/matplotlib/rcsetup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1091,6 +1091,7 @@ def _validate_linestyle(ls):
10911091
'text.latex.preview': [False, validate_bool],
10921092
'text.hinting': ['auto', validate_hinting],
10931093
'text.hinting_factor': [8, validate_int],
1094+
'text.kerning_factor': [0, validate_int],
10941095
'text.antialiased': [True, validate_bool],
10951096

10961097
'mathtext.cal': ['cursive', validate_font_properties],

lib/matplotlib/tests/test_artist.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,9 @@ def test_remove():
201201

202202
@image_comparison(["default_edges.png"], remove_text=True, style='default')
203203
def test_default_edges():
204+
# Remove this line when this test image is regenerated.
205+
plt.rcParams['text.kerning_factor'] = 6
206+
204207
fig, [[ax1, ax2], [ax3, ax4]] = plt.subplots(2, 2)
205208

206209
ax1.plot(np.arange(10), np.arange(10), 'x',

lib/matplotlib/tests/test_axes.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ def test_get_labels():
4848

4949
@image_comparison(['acorr.png'], style='mpl20')
5050
def test_acorr():
51+
# Remove this line when this test image is regenerated.
52+
plt.rcParams['text.kerning_factor'] = 6
53+
5154
np.random.seed(19680801)
5255
n = 512
5356
x = np.random.normal(0, 1, n).cumsum()
@@ -5730,6 +5733,9 @@ def test_axisbelow():
57305733

57315734
@image_comparison(['titletwiny.png'], style='mpl20')
57325735
def test_titletwiny():
5736+
# Remove this line when this test image is regenerated.
5737+
plt.rcParams['text.kerning_factor'] = 6
5738+
57335739
# Test that title is put above xlabel if xlabel at top
57345740
fig, ax = plt.subplots()
57355741
fig.subplots_adjust(top=0.8)

lib/matplotlib/tests/test_image.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@
2727
@image_comparison(['image_interps'], style='mpl20')
2828
def test_image_interps():
2929
'make the basic nearest, bilinear and bicubic interps'
30+
# Remove this line when this test image is regenerated.
31+
plt.rcParams['text.kerning_factor'] = 6
32+
3033
X = np.arange(100)
3134
X = X.reshape(5, 20)
3235

lib/matplotlib/tests/test_legend.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,9 @@ def test_legend_expand():
188188

189189
@image_comparison(['hatching'], remove_text=True, style='default')
190190
def test_hatching():
191+
# Remove this line when this test image is regenerated.
192+
plt.rcParams['text.kerning_factor'] = 6
193+
191194
fig, ax = plt.subplots()
192195

193196
# Patches

lib/matplotlib/tests/test_text.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,9 @@ def test_multiline():
130130

131131
@image_comparison(['multiline2'], style='mpl20')
132132
def test_multiline2():
133+
# Remove this line when this test image is regenerated.
134+
plt.rcParams['text.kerning_factor'] = 6
135+
133136
fig, ax = plt.subplots()
134137

135138
ax.set_xlim([0, 1.4])
@@ -580,6 +583,9 @@ def test_annotation_update():
580583

581584
@image_comparison(['large_subscript_title.png'], style='mpl20')
582585
def test_large_subscript_title():
586+
# Remove this line when this test image is regenerated.
587+
plt.rcParams['text.kerning_factor'] = 6
588+
583589
fig, axs = plt.subplots(1, 2, figsize=(9, 2.5), constrained_layout=True)
584590
ax = axs[0]
585591
ax.set_title(r'$\sum_{i} x_i$')

lib/matplotlib/tests/test_widgets.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,9 @@ def test_CheckButtons():
264264

265265
@image_comparison(['check_radio_buttons.png'], style='mpl20', remove_text=True)
266266
def test_check_radio_buttons_image():
267+
# Remove this line when this test image is regenerated.
268+
plt.rcParams['text.kerning_factor'] = 6
269+
267270
get_ax()
268271
plt.subplots_adjust(left=0.3)
269272
rax1 = plt.axes([0.05, 0.7, 0.15, 0.15])

lib/mpl_toolkits/tests/test_axisartist_axis_artist.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ def test_ticks():
2626

2727
@image_comparison(['axis_artist_labelbase.png'], style='default')
2828
def test_labelbase():
29+
# Remove this line when this test image is regenerated.
30+
plt.rcParams['text.kerning_factor'] = 6
31+
2932
fig, ax = plt.subplots()
3033

3134
ax.plot([0.5], [0.5], "o")
@@ -40,6 +43,9 @@ def test_labelbase():
4043

4144
@image_comparison(['axis_artist_ticklabels.png'], style='default')
4245
def test_ticklabels():
46+
# Remove this line when this test image is regenerated.
47+
plt.rcParams['text.kerning_factor'] = 6
48+
4349
fig, ax = plt.subplots()
4450

4551
ax.xaxis.set_visible(False)
@@ -72,6 +78,9 @@ def test_ticklabels():
7278

7379
@image_comparison(['axis_artist.png'], style='default')
7480
def test_axis_artist():
81+
# Remove this line when this test image is regenerated.
82+
plt.rcParams['text.kerning_factor'] = 6
83+
7584
fig, ax = plt.subplots()
7685

7786
ax.xaxis.set_visible(False)

lib/mpl_toolkits/tests/test_axisartist_axislines.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111

1212
@image_comparison(['SubplotZero.png'], style='default')
1313
def test_SubplotZero():
14+
# Remove this line when this test image is regenerated.
15+
plt.rcParams['text.kerning_factor'] = 6
16+
1417
fig = plt.figure()
1518

1619
ax = SubplotZero(fig, 1, 1, 1)
@@ -29,6 +32,9 @@ def test_SubplotZero():
2932

3033
@image_comparison(['Subplot.png'], style='default')
3134
def test_Subplot():
35+
# Remove this line when this test image is regenerated.
36+
plt.rcParams['text.kerning_factor'] = 6
37+
3238
fig = plt.figure()
3339

3440
ax = Subplot(fig, 1, 1, 1)

lib/mpl_toolkits/tests/test_axisartist_floating_axes.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,9 @@ def test_curvelinear3():
7474

7575
@image_comparison(['curvelinear4.png'], style='default', tol=0.015)
7676
def test_curvelinear4():
77+
# Remove this line when this test image is regenerated.
78+
plt.rcParams['text.kerning_factor'] = 6
79+
7780
fig = plt.figure(figsize=(5, 5))
7881

7982
tr = (mtransforms.Affine2D().scale(np.pi / 180, 1) +

lib/mpl_toolkits/tests/test_axisartist_grid_helper_curvelinear.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,9 @@ def inverted(self):
8585
@image_comparison(['polar_box.png'], style='default',
8686
tol={'aarch64': 0.04}.get(platform.machine(), 0.03))
8787
def test_polar_box():
88+
# Remove this line when this test image is regenerated.
89+
plt.rcParams['text.kerning_factor'] = 6
90+
8891
fig = plt.figure(figsize=(5, 5))
8992

9093
# PolarAxes.PolarTransform takes radian. However, we want our coordinate
@@ -145,6 +148,9 @@ def test_polar_box():
145148

146149
@image_comparison(['axis_direction.png'], style='default', tol=0.03)
147150
def test_axis_direction():
151+
# Remove this line when this test image is regenerated.
152+
plt.rcParams['text.kerning_factor'] = 6
153+
148154
fig = plt.figure(figsize=(5, 5))
149155

150156
# PolarAxes.PolarTransform takes radian. However, we want our coordinate

matplotlibrc.template

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,10 @@
316316
#text.hinting_factor : 8 ## Specifies the amount of softness for hinting in the
317317
## horizontal direction. A value of 1 will hint to full
318318
## pixels. A value of 2 will hint to half pixels etc.
319+
#text.kerning_factor : 0 ## Specifies the scaling factor for kerning values. This
320+
## is provided solely to allow old test images to remain
321+
## unchanged. Set to 6 to obtain previous behavior. Values
322+
## other than 0 or 6 have no defined meaning.
319323
#text.antialiased : True ## If True (default), the text will be antialiased.
320324
## This only affects the Agg backend.
321325

src/ft2font.cpp

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -525,6 +525,9 @@ FT2Font::FT2Font(FT_Open_Args &open_args, long hinting_factor_) : image(), face(
525525
throw_ft_error("Can not load face", error);
526526
}
527527

528+
// set default kerning factor to 0, i.e., no kerning manipulation
529+
kerning_factor = 0;
530+
528531
// set a default fontsize 12 pt at 72dpi
529532
hinting_factor = hinting_factor_;
530533

@@ -568,7 +571,7 @@ void FT2Font::clear()
568571
void FT2Font::set_size(double ptsize, double dpi)
569572
{
570573
FT_Error error = FT_Set_Char_Size(
571-
face, (long)(ptsize * 64), 0, (unsigned int)(dpi * hinting_factor), (unsigned int)dpi);
574+
face, (FT_F26Dot6)(ptsize * 64), 0, (FT_UInt)(dpi * hinting_factor), (FT_UInt)dpi);
572575
if (error) {
573576
throw_ft_error("Could not set the fontsize", error);
574577
}
@@ -602,15 +605,22 @@ int FT2Font::get_kerning(FT_UInt left, FT_UInt right, FT_UInt mode)
602605
FT_Vector delta;
603606

604607
if (!FT_Get_Kerning(face, left, right, mode, &delta)) {
605-
return (int)(delta.x) / (hinting_factor << 6);
608+
return (int)(delta.x) / (hinting_factor << kerning_factor);
606609
} else {
607610
return 0;
608611
}
609612
}
610613

614+
void FT2Font::set_kerning_factor(int factor)
615+
{
616+
kerning_factor = factor;
617+
}
618+
611619
void FT2Font::set_text(
612620
size_t N, uint32_t *codepoints, double angle, FT_Int32 flags, std::vector<double> &xys)
613621
{
622+
FT_Matrix matrix; /* transformation matrix */
623+
614624
angle = angle / 360.0 * 2 * M_PI;
615625

616626
// this computes width and height in subpixels so we have to divide by 64
@@ -638,7 +648,7 @@ void FT2Font::set_text(
638648
if (use_kerning && previous && glyph_index) {
639649
FT_Vector delta;
640650
FT_Get_Kerning(face, previous, glyph_index, FT_KERNING_DEFAULT, &delta);
641-
pen.x += (delta.x << 10) / (hinting_factor << 16);
651+
pen.x += delta.x / (hinting_factor << kerning_factor);
642652
}
643653
if (FT_Error error = FT_Load_Glyph(face, glyph_index, flags)) {
644654
throw_ft_error("Could not load glyph", error);
@@ -659,7 +669,7 @@ void FT2Font::set_text(
659669
xys.push_back(pen.x);
660670
xys.push_back(pen.y);
661671

662-
FT_Glyph_Get_CBox(thisGlyph, ft_glyph_bbox_subpixels, &glyph_bbox);
672+
FT_Glyph_Get_CBox(thisGlyph, FT_GLYPH_BBOX_SUBPIXELS, &glyph_bbox);
663673

664674
bbox.xMin = std::min(bbox.xMin, glyph_bbox.xMin);
665675
bbox.xMax = std::max(bbox.xMax, glyph_bbox.xMax);

src/ft2font.h

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ class FT2Font
7474
void set_text(
7575
size_t N, uint32_t *codepoints, double angle, FT_Int32 flags, std::vector<double> &xys);
7676
int get_kerning(FT_UInt left, FT_UInt right, FT_UInt mode);
77+
void set_kerning_factor(int factor);
7778
void load_char(long charcode, FT_Int32 flags);
7879
void load_glyph(FT_UInt glyph_index, FT_Int32 flags);
7980
void get_width_height(long *width, long *height);
@@ -117,17 +118,12 @@ class FT2Font
117118
private:
118119
FT2Image image;
119120
FT_Face face;
120-
FT_Matrix matrix; /* transformation matrix */
121121
FT_Vector pen; /* untransformed origin */
122122
std::vector<FT_Glyph> glyphs;
123-
std::vector<FT_Vector> pos;
124123
FT_BBox bbox;
125124
FT_Pos advance;
126-
double ptsize;
127-
double dpi;
128125
long hinting_factor;
129-
130-
void set_scalable_attributes();
126+
int kerning_factor;
131127

132128
// prevent copying
133129
FT2Font(const FT2Font &);

src/ft2font_wrapper.cpp

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -553,10 +553,12 @@ static int PyFT2Font_init(PyFT2Font *self, PyObject *args, PyObject *kwds)
553553
PyObject *fname;
554554
FT_Open_Args open_args;
555555
long hinting_factor = 8;
556-
const char *names[] = { "filename", "hinting_factor", NULL };
556+
int kerning_factor = 0;
557+
const char *names[] = { "filename", "hinting_factor", "_kerning_factor", NULL };
557558

558559
if (!PyArg_ParseTupleAndKeywords(
559-
args, kwds, "O|l:FT2Font", (char **)names, &fname, &hinting_factor)) {
560+
args, kwds, "O|l$i:FT2Font", (char **)names, &fname,
561+
&hinting_factor, &kerning_factor)) {
560562
return -1;
561563
}
562564

@@ -567,6 +569,8 @@ static int PyFT2Font_init(PyFT2Font *self, PyObject *args, PyObject *kwds)
567569
CALL_CPP_FULL(
568570
"FT2Font", (self->x = new FT2Font(open_args, hinting_factor)), PyFT2Font_fail(self), -1);
569571

572+
CALL_CPP("FT2Font->set_kerning_factor", (self->x->set_kerning_factor(kerning_factor)));
573+
570574
Py_INCREF(fname);
571575
self->fname = fname;
572576

0 commit comments

Comments
 (0)