Skip to content

Commit 2c47630

Browse files
committed
Further simplify the flow of pdf text output.
... and, as a side benefit, correctly kern between 1-byte and 2-byte chunks.
1 parent eb6664f commit 2c47630

File tree

2 files changed

+50
-103
lines changed

2 files changed

+50
-103
lines changed

lib/matplotlib/_text_layout.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from .ft2font import KERNING_DEFAULT, LOAD_NO_HINTING
66

77

8-
def layout(string, font, *, x0=0, kern_mode=KERNING_DEFAULT):
8+
def layout(string, font, *, kern_mode=KERNING_DEFAULT):
99
"""
1010
Render *string* with *font*. For each character in *string*, yield a
1111
(character-index, x-position) pair. When such a pair is yielded, the
@@ -17,8 +17,6 @@ def layout(string, font, *, x0=0, kern_mode=KERNING_DEFAULT):
1717
The string to be rendered.
1818
font : FT2Font
1919
The font.
20-
x0 : float
21-
The initial x-value
2220
kern_mode : int
2321
A FreeType kerning mode.
2422
@@ -27,7 +25,7 @@ def layout(string, font, *, x0=0, kern_mode=KERNING_DEFAULT):
2725
character_index : int
2826
x_position : float
2927
"""
30-
x = x0
28+
x = 0
3129
last_char_idx = None
3230
for char in string:
3331
char_idx = font.get_char_index(ord(char))

lib/matplotlib/backends/backend_pdf.py

Lines changed: 48 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -2092,17 +2092,6 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
20922092

20932093
# TODO: combine consecutive texts into one BT/ET delimited section
20942094

2095-
# This function is rather complex, since there is no way to
2096-
# access characters of a Type 3 font with codes > 255. (Type
2097-
# 3 fonts can not have a CIDMap). Therefore, we break the
2098-
# string into chunks, where each chunk contains exclusively
2099-
# 1-byte or exclusively 2-byte characters, and output each
2100-
# chunk a separate command. 1-byte characters use the regular
2101-
# text show command (Tj), whereas 2-byte characters use the
2102-
# use XObject command (Do). If using Type 42 fonts, all of
2103-
# this complication is avoided, but of course, those fonts can
2104-
# not be subsetted.
2105-
21062095
self.check_gc(gc, gc._rgb)
21072096
if ismath:
21082097
return self.draw_mathtext(gc, x, y, s, prop, angle)
@@ -2115,116 +2104,76 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
21152104
else:
21162105
font = self._get_font_ttf(prop)
21172106
self.track_characters(font, s)
2118-
font.set_text(s, 0.0, flags=LOAD_NO_HINTING)
2119-
21202107
fonttype = rcParams['pdf.fonttype']
2121-
21222108
# We can't subset all OpenType fonts, so switch to Type 42
21232109
# in that case.
21242110
if is_opentype_cff_font(font.fname):
21252111
fonttype = 42
21262112

2127-
def check_simple_method(s):
2128-
"""
2129-
Determine if we should use the simple or woven method to output
2130-
this text, and chunks the string into 1-byte and 2-byte sections if
2131-
necessary.
2132-
"""
2133-
use_simple_method = True
2134-
chunks = []
2135-
2136-
if not rcParams['pdf.use14corefonts']:
2137-
if fonttype == 3 and not isinstance(s, bytes) and len(s) != 0:
2138-
# Break the string into chunks where each chunk is either
2139-
# a string of chars <= 255, or a single character > 255.
2140-
s = str(s)
2141-
for c in s:
2142-
if ord(c) <= 255:
2143-
char_type = 1
2144-
else:
2145-
char_type = 2
2146-
if len(chunks) and chunks[-1][0] == char_type:
2147-
chunks[-1][1].append(c)
2148-
else:
2149-
chunks.append((char_type, [c]))
2150-
use_simple_method = (len(chunks) == 1 and
2151-
chunks[-1][0] == 1)
2152-
return use_simple_method, chunks
2153-
2154-
def draw_text_simple():
2155-
"""Outputs text using the simple method."""
2113+
# If fonttype != 3 or there are no multibyte characters, emit the whole
2114+
# string at once.
2115+
if fonttype != 3 or all(ord(char) <= 255 for char in s):
21562116
self.file.output(Op.begin_text,
2157-
self.file.fontName(prop),
2158-
fontsize,
2159-
Op.selectfont)
2117+
self.file.fontName(prop), fontsize, Op.selectfont)
21602118
self._setup_textpos(x, y, angle)
21612119
self.file.output(self.encode_string(s, fonttype), Op.show,
21622120
Op.end_text)
21632121

2164-
def draw_text_woven(chunks):
2165-
"""
2166-
Outputs text using the woven method, alternating between chunks of
2167-
1-byte and 2-byte characters. Only used for Type 3 fonts.
2168-
"""
2169-
chunks = [(a, ''.join(b)) for a, b in chunks]
2170-
2122+
# There is no way to access multibyte characters of Type 3 fonts, as
2123+
# they cannot have a CIDMap. Therefore, in this case we break the
2124+
# string into chunks, where each chunk contains either a string of
2125+
# consecutive 1-byte characters or a single multibyte character. Each
2126+
# chunk is emitted with a separate command: 1-byte characters use the
2127+
# regular text show command (Tj), whereas multibyte characters use
2128+
# the XObject command (Do). (If using Type 42 fonts, all of this
2129+
# complication is avoided, but of course, those fonts can not be
2130+
# subsetted.)
2131+
else:
2132+
singlebyte_chunks = [] # List of (start_x, list-of-1-byte-chars).
2133+
multibyte_glyphs = [] # List of (start_x, glyph_index).
2134+
prev_was_singlebyte = False
2135+
for char, (glyph_idx, glyph_x) in zip(
2136+
s,
2137+
_text_layout.layout(s, font, kern_mode=KERNING_UNFITTED)):
2138+
if ord(char) <= 255:
2139+
if prev_was_singlebyte:
2140+
singlebyte_chunks[-1][1].append(char)
2141+
else:
2142+
singlebyte_chunks.append((glyph_x, [char]))
2143+
prev_was_singlebyte = True
2144+
else:
2145+
multibyte_glyphs.append((glyph_x, glyph_idx))
2146+
prev_was_singlebyte = False
21712147
# Do the rotation and global translation as a single matrix
21722148
# concatenation up front
21732149
self.file.output(Op.gsave)
21742150
a = math.radians(angle)
21752151
self.file.output(math.cos(a), math.sin(a),
21762152
-math.sin(a), math.cos(a),
21772153
x, y, Op.concat_matrix)
2178-
2179-
# Output all the 1-byte characters in a BT/ET group.
2154+
# Emit all the 1-byte characters in a BT/ET group.
21802155
self.file.output(Op.begin_text,
2181-
self.file.fontName(prop),
2182-
fontsize,
2183-
Op.selectfont)
2184-
newx = oldx = 0
2185-
for chunk_type, chunk in chunks:
2186-
if chunk_type == 1:
2187-
self._setup_textpos(newx, 0, 0, oldx, 0, 0)
2188-
self.file.output(self.encode_string(chunk, fonttype),
2189-
Op.show)
2190-
oldx = newx
2191-
# Update newx to include the advance from this chunk,
2192-
# regardless of its mode...
2193-
for char_idx, char_x in _text_layout.layout(
2194-
chunk, font, x0=newx, kern_mode=KERNING_UNFITTED):
2195-
pass
2196-
newx = char_x + ( # ... including the last character's advance
2197-
font.load_glyph(char_idx, flags=LOAD_NO_HINTING)
2198-
.linearHoriAdvance / 65536)
2156+
self.file.fontName(prop), fontsize, Op.selectfont)
2157+
prev_start_x = 0
2158+
for start_x, chars in singlebyte_chunks:
2159+
self._setup_textpos(start_x, 0, 0, prev_start_x, 0, 0)
2160+
self.file.output(self.encode_string(''.join(chars), fonttype),
2161+
Op.show)
2162+
prev_start_x = start_x
21992163
self.file.output(Op.end_text)
2200-
2201-
# Then output all the 2-byte characters.
2202-
newx = 0
2203-
for chunk_type, chunk in chunks:
2204-
for char_idx, char_x in _text_layout.layout(
2205-
chunk, font, x0=newx, kern_mode=KERNING_UNFITTED):
2206-
if chunk_type == 2:
2207-
glyph_name = font.get_glyph_name(char_idx)
2208-
self.file.output(Op.gsave)
2209-
self.file.output(0.001 * fontsize, 0,
2210-
0, 0.001 * fontsize,
2211-
char_x, 0, Op.concat_matrix)
2212-
name = self.file._get_xobject_symbol_name(
2213-
font.fname, glyph_name)
2214-
self.file.output(Name(name), Op.use_xobject)
2215-
self.file.output(Op.grestore)
2216-
newx = char_x + ( # ... including the last character's advance
2217-
font.load_glyph(char_idx, flags=LOAD_NO_HINTING)
2218-
.linearHoriAdvance / 65536)
2219-
2164+
# Then emit all the multibyte characters, one at a time.
2165+
for start_x, glyph_idx in multibyte_glyphs:
2166+
glyph_name = font.get_glyph_name(glyph_idx)
2167+
self.file.output(Op.gsave)
2168+
self.file.output(0.001 * fontsize, 0,
2169+
0, 0.001 * fontsize,
2170+
start_x, 0, Op.concat_matrix)
2171+
name = self.file._get_xobject_symbol_name(
2172+
font.fname, glyph_name)
2173+
self.file.output(Name(name), Op.use_xobject)
2174+
self.file.output(Op.grestore)
22202175
self.file.output(Op.grestore)
22212176

2222-
use_simple_method, chunks = check_simple_method(s)
2223-
if use_simple_method:
2224-
return draw_text_simple()
2225-
else:
2226-
return draw_text_woven(chunks)
2227-
22282177
def new_gc(self):
22292178
# docstring inherited
22302179
return GraphicsContextPdf(self.file)

0 commit comments

Comments
 (0)