10000 Include kerning when outputting pdf strings. · matplotlib/matplotlib@1e1883b · GitHub
[go: up one dir, main page]

Skip to content

Commit 1e1883b

Browse files
committed
Include kerning when outputting pdf strings.
1 parent c1f6515 commit 1e1883b

File tree

6 files changed

+49
-27
lines changed

6 files changed

+49
-27
lines changed

lib/matplotlib/_text_layout.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,15 @@
22
Text layouting utilities.
33
"""
44

5+
import dataclasses
6+
57
from .ft2font import KERNING_DEFAULT, LOAD_NO_HINTING
68

79

10+
LayoutItem = dataclasses.make_dataclass(
11+
"LayoutItem", ["char", "glyph_idx", "x", "prev_kern"])
12+
13+
814
def layout(string, font, *, kern_mode=KERNING_DEFAULT):
915
"""
1016
Render *string* with *font*. For each character in *string*, yield a
@@ -26,13 +32,13 @@ def layout(string, font, *, kern_mode=KERNING_DEFAULT):
2632
x_position : float
2733
"""
2834
x = 0
29-
last_glyph_idx = None
35+
prev_glyph_idx = None
3036
for char in string:
3137
glyph_idx = font.get_char_index(ord(char))
32-
kern = (font.get_kerning(last_glyph_idx, glyph_idx, kern_mode)
33-
if last_glyph_idx is not None else 0) / 64
38+
kern = (font.get_kerning(prev_glyph_idx, glyph_idx, kern_mode) / 64
39+
if prev_glyph_idx is not None else 0.)
3440
x += kern
3541
glyph = font.load_glyph(glyph_idx, flags=LOAD_NO_HINTING)
36-
yield glyph_idx, x
42+
yield LayoutItem(char, glyph_idx, x, kern)
3743
x += glyph.linearHoriAdvance / 65536
38-
last_glyph_idx = glyph_idx
44+
prev_glyph_idx = glyph_idx

lib/matplotlib/backends/backend_pdf.py

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2281,21 +2281,23 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
22812281
# complication is avoided, but of course, those fonts can not be
22822282
# subsetted.)
22832283
else:
2284-
singlebyte_chunks = [] # List of (start_x, list-of-1-byte-chars).
2285-
multibyte_glyphs = [] # List of (start_x, glyph_index).
2286-
prev_was_singlebyte = False
2287-
for char, (glyph_idx, glyph_x) in zip(
2288-
s,
2289-
_text_layout.layout(s, font, kern_mode=KERNING_UNFITTED)):
2290-
if ord(char) <= 255:
2291-
if prev_was_singlebyte:
2292-
singlebyte_chunks[-1][1].append(char)
2293-
else:
2294-
singlebyte_chunks.append((glyph_x, [char]))
2295-
prev_was_singlebyte = True
2284+
# List of (start_x, [prev_kern, char, char, ...]), w/o zero kerns.
2285+
singlebyte_chunks = []
2286+
# List of (start_x, glyph_index).
2287+
multibyte_glyphs = []
2288+
prev_was_multibyte = True
2289+
for item in _text_layout.layout(
2290+
s, font, kern_mode=KERNING_UNFITTED):
2291+
if ord(item.char) <= 255:
2292+
if prev_was_multibyte:
2293+
singlebyte_chunks.append((item.x, []))
2294+
if item.prev_kern:
2295+
singlebyte_chunks[-1][1].append(item.prev_kern)
2296+
singlebyte_chunks[-1][1].append(item.char)
2297+
prev_was_multibyte = False
22962298
else:
2297-
multibyte_glyphs.append((glyph_x, glyph_idx))
2298-
prev_was_singlebyte = False
2299+
multibyte_glyphs.append((item.x, item.glyph_idx))
2300+
prev_was_multibyte = True
22992301
# Do the rotation and global translation as a single matrix
23002302
# concatenation up front
23012303
self.file.output(Op.gsave)
@@ -2307,10 +2309,15 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
23072309
self.file.output(Op.begin_text,
23082310
self.file.fontName(prop), fontsize, Op.selectfont)
23092311
prev_start_x = 0
2310-
for start_x, chars in singlebyte_chunks:
2312+
for start_x, kerns_or_chars in singlebyte_chunks:
23112313
self._setup_textpos(start_x, 0, 0, prev_start_x, 0, 0)
2312-
self.file.output(self.encode_string(''.join(chars), fonttype),
2313-
Op.show)
2314+
self.file.output(
2315+
# See pdf spec "Text space details" for the 1000/fontsize
2316+
# (aka. 1000/T_fs) factor.
2317+
[-1000 * next(group) / fontsize if tp == float # a kern
2318+
else self.encode_string("".join(group), fonttype)
2319+
for tp, group in itertools.groupby(kerns_or_chars, type)],
2320+
Op.showkern)
23142321
prev_start_x = start_x
23152322
self.file.output(Op.end_text)
23162323
# Then emit all the multibyte characters, one at a time.

lib/matplotlib/backends/backend_ps.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -585,8 +585,9 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
585585
self.set_font(ps_name, prop.get_size_in_points())
586586

587587
thetext = '\n'.join(
588-
'%f 0 m /%s glyphshow' % (x, font.get_glyph_name(glyph_idx))
589-
for glyph_idx, x in _text_layout.layout(s, font))
588+
'{:f} 0 m /{:s} glyphshow'
589+
.format(item.x, font.get_glyph_name(item.glyph_idx))
590+
for item in _text_layout.layout(s, font))
590591
self._pswriter.write(f"""\
591592
gsave
592593
{x:f} {y:f} translate
Binary file not shown.

lib/matplotlib/tests/test_backend_pdf.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,3 +271,11 @@ def test_empty_rasterized():
271271
fig, ax = plt.subplots()
272272
ax.plot([], [], rasterized=True)
273273
fig.savefig(io.BytesIO(), format="pdf")
274+
275+
276+
@image_comparison(['kerning.pdf'])
277+
def test_kerning():
278+
fig = plt.figure()
279+
s = "AVAVAVAVAVAVAVAV€AAVV"
280+
fig.text(0, .25, s, size=5)
281+
fig.text(0, .75, s, size=20)

lib/matplotlib/textpath.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -149,10 +149,10 @@ def get_glyphs_with_font(self, font, s, glyph_map=None,
149149

150150
xpositions = []
151151
glyph_ids = []
152-
for char, (_, x) in zip(s, _text_layout.layout(s, font)):
153-
char_id = self._get_char_id(font, ord(char))
152+
for item in _text_layout.layout(s, font):
153+
char_id = self._get_char_id(font, ord(item.char))
154154
glyph_ids.append(char_id)
155-
xpositions.append(x)
155+
xpositions.append(item.x)
156156
if char_id not in glyph_map:
157157
glyph_map_new[char_id] = font.get_path()
158158

0 commit comments

Comments
 (0)
0