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

Skip to content

Include kerning when outputting pdf strings. #18517

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Sep 19, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions lib/matplotlib/_text_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,15 @@
Text layouting utilities.
"""

import dataclasses

from .ft2font import KERNING_DEFAULT, LOAD_NO_HINTING


LayoutItem = dataclasses.make_dataclass(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are dataclasses getting us here that a named tuple would not?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The thing is not iterable, which avoids accidentally relying on an unpacking API (x, y = ...) which would be a pain to modify (i.e. you can't add another field while maintaining a sensible deprecation).
Of course this is private API so in a sense we could do whatever we want, but as a general rule I think dataclasses are nicer (but note that I went for the "functional" declaration API because I certainly wouldn't want to be the person introducing type hints in the codebase :))

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is a compelling agreement 👍 .

"LayoutItem", ["char", "glyph_idx", "x", "prev_kern"])


def layout(string, font, *, kern_mode=KERNING_DEFAULT):
"""
Render *string* with *font*. For each character in *string*, yield a
Expand All @@ -26,13 +32,13 @@ def layout(string, font, *, kern_mode=KERNING_DEFAULT):
x_position : float
"""
x = 0
last_glyph_idx = None
prev_glyph_idx = None
for char in string:
glyph_idx = font.get_char_index(ord(char))
kern = (font.get_kerning(last_glyph_idx, glyph_idx, kern_mode)
if last_glyph_idx is not None else 0) / 64
kern = (font.get_kerning(prev_glyph_idx, glyph_idx, kern_mode) / 64
if prev_glyph_idx is not None else 0.)
x += kern
glyph = font.load_glyph(glyph_idx, flags=LOAD_NO_HINTING)
yield glyph_idx, x
yield LayoutItem(char, glyph_idx, x, kern)
x += glyph.linearHoriAdvance / 65536
last_glyph_idx = glyph_idx
prev_glyph_idx = glyph_idx
41 changes: 24 additions & 17 deletions lib/matplotlib/backends/backend_pdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -2281,21 +2281,23 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
# complication is avoided, but of course, those fonts can not be
# subsetted.)
else:
singlebyte_chunks = [] # List of (start_x, list-of-1-byte-chars).
multibyte_glyphs = [] # List of (start_x, glyph_index).
prev_was_singlebyte = False
for char, (glyph_idx, glyph_x) in zip(
s,
_text_layout.layout(s, font, kern_mode=KERNING_UNFITTED)):
if ord(char) <= 255:
if prev_was_singlebyte:
singlebyte_chunks[-1][1].append(char)
else:
singlebyte_chunks.append((glyph_x, [char]))
prev_was_singlebyte = True
# List of (start_x, [prev_kern, char, char, ...]), w/o zero kerns.
singlebyte_chunks = []
# List of (start_x, glyph_index).
multibyte_glyphs = []
prev_was_multibyte = True
for item in _text_layout.layout(
s, font, kern_mode=KERNING_UNFITTED):
if ord(item.char) <= 255:
if prev_was_multibyte:
singlebyte_chunks.append((item.x, []))
if item.prev_kern:
singlebyte_chunks[-1][1].append(item.prev_kern)
singlebyte_chunks[-1][1].append(item.char)
prev_was_multibyte = False
else:
multibyte_glyphs.append((glyph_x, glyph_idx))
prev_was_singlebyte = False
multibyte_glyphs.append((item.x, item.glyph_idx))
prev_was_multibyte = True
# Do the rotation and global translation as a single matrix
# concatenation up front
self.file.output(Op.gsave)
Expand All @@ -2307,10 +2309,15 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
self.file.output(Op.begin_text,
self.file.fontName(prop), fontsize, Op.selectfont)
prev_start_x = 0
for start_x, chars in singlebyte_chunks:
for start_x, kerns_or_chars in singlebyte_chunks:
self._setup_textpos(start_x, 0, 0, prev_start_x, 0, 0)
self.file.output(self.encode_string(''.join(chars), fonttype),
Op.show)
self.file.output(
# See pdf spec "Text space details" for the 1000/fontsize
# (aka. 1000/T_fs) factor.
[-1000 * next(group) / fontsize if tp == float # a kern
else self.encode_string("".join(group), fonttype)
for tp, group in itertools.groupby(kerns_or_chars, type)],
Op.showkern)
prev_start_x = start_x
self.file.output(Op.end_text)
# Then emit all the multibyte characters, one at a time.
Expand Down
5 changes: 3 additions & 2 deletions lib/matplotlib/backends/backend_ps.py
Original file line number Diff line number Diff line change
Expand Up @@ -585,8 +585,9 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
self.set_font(ps_name, prop.get_size_in_points())

thetext = '\n'.join(
'%f 0 m /%s glyphshow' % (x, font.get_glyph_name(glyph_idx))
for glyph_idx, x in _text_layout.layout(s, font))
'{:f} 0 m /{:s} glyphshow'
.format(item.x, font.get_glyph_name(item.glyph_idx))
for item in _text_layout.layout(s, font))
self._pswriter.write(f"""\
gsave
{x:f} {y:f} translate
Expand Down
Binary file not shown.
8 changes: 8 additions & 0 deletions lib/matplotlib/tests/test_backend_pdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,3 +271,11 @@ def test_empty_rasterized():
fig, ax = plt.subplots()
ax.plot([], [], rasterized=True)
fig.savefig(io.BytesIO(), format="pdf")


@image_comparison(['kerning.pdf'])
def test_kerning():
fig = plt.figure()
s = "AVAVAVAVAVAVAVAV€AAVV"
fig.text(0, .25, s, size=5)
fig.text(0, .75, s, size=20)
6 changes: 3 additions & 3 deletions lib/matplotlib/textpath.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,10 +149,10 @@ def get_glyphs_with_font(self, font, s, glyph_map=None,

xpositions = []
glyph_ids = []
for char, (_, x) in zip(s, _text_layout.layout(s, font)):
char_id = self._get_char_id(font, ord(char))
for item in _text_layout.layout(s, font):
char_id = self._get_char_id(font, ord(item.char))
glyph_ids.append(char_id)
xpositions.append(x)
xpositions.append(item.x)
if char_id not in glyph_map:
glyph_map_new[char_id] = font.get_path()

Expand Down