Skip to content

Commit 586edd9

Browse files
committed
Stop using ttconv for pdf.
Quite a bit of work goes into reproducing quirks of the old ttconv code in order not to update baseline images, but this can easily be dropped once the new baseline images system goes in.
1 parent 7041e3c commit 586edd9

File tree

3 files changed

+76
-35
lines changed

3 files changed

+76
-35
lines changed

lib/matplotlib/backends/backend_pdf.py

+46-10
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@
4242
from matplotlib.path import Path
4343
from matplotlib.dates import UTC
4444
from matplotlib import _path
45-
from matplotlib import _ttconv
4645
from . import _backend_pdf_ps
4746

4847
_log = logging.getLogger(__name__)
@@ -556,6 +555,50 @@ def _flush(self):
556555
self.compressobj = None
557556

558557

558+
def _get_pdf_charprocs(font_path, glyph_ids):
559+
font = get_font(font_path, hinting_factor=1)
560+
conv = 1000 / font.units_per_EM # Conversion to PS units (1/1000's).
561+
procs = {}
562+
for glyph_id in glyph_ids:
563+
g = font.load_glyph(glyph_id, LOAD_NO_SCALE)
564+
# NOTE: We should be using round(), but instead use
565+
# "(x+.5).astype(int)" to keep backcompat with the old ttconv code
566+
# (this is different for negative x's).
567+
d1 = (np.array([g.horiAdvance, 0, *g.bbox]) * conv + .5).astype(int)
568+
v, c = font.get_path()
569+
v = (v * 64).astype(int) # Back to TrueType's internal units (1/64's).
570+
# Backcompat with old ttconv code: control points between two quads are
571+
# omitted if they are exactly at the midpoint between the control of
572+
# the quad before and the quad after, but ttconv used to interpolate
573+
# *after* conversion to PS units, causing floating point errors. Here
574+
# we reproduce ttconv's logic, detecting these "implicit" points and
575+
# re-interpolating them.
576+
quads, = np.nonzero(c == 3)
577+
quads_on = quads[1::2]
578+
quads_mid_on = np.array(
579+
sorted({*quads_on} & {*quads - 1} & {*quads + 1}), int)
580+
implicit = quads_mid_on[
581+
(v[quads_mid_on] # As above, use astype(int), not // division
582+
== ((v[quads_mid_on - 1] + v[quads_mid_on + 1]) / 2).astype(int))
583+
.all(axis=1)]
584+
if (font.postscript_name, glyph_id) in [
585+
("DejaVuSerif-Italic", 77), # j
586+
("DejaVuSerif-Italic", 135), # \AA
587+
]:
588+
v[:, 0] -= 1 # Hard-coded backcompat (FreeType shifts glyph by 1).
589+
v = (v * conv + .5).astype(int) # As above re: truncation vs rounding.
590+
v[implicit] = (( # Fix implicit points; again, truncate.
591+
(v[implicit - 1] + v[implicit + 1]) / 2).astype(int))
592+
procs[font.get_glyph_name(glyph_id)] = (
593+
" ".join(map(str, d1)).encode("ascii") + b" d1\n"
594+
+ _path.convert_to_string(
595+
Path(v, c), None, None, False, None, -1,
596+
# no code for quad Beziers triggers auto-conversion to cubics.
597+
[b"m", b"l", b"", b"c", b"h"], True)
598+
+ b"f")
599+
return procs
600+
601+
559602
class PdfFile:
560603
"""PDF file object."""
561604

@@ -1089,15 +1132,8 @@ def get_char_width(charcode):
10891132
differencesArray.append(Name(name))
10901133
last_c = c
10911134

1092-
# Make the charprocs array (using ttconv to generate the
1093-
# actual outlines)
1094-
try:
1095-
rawcharprocs = _ttconv.get_pdf_charprocs(
1096-
os.fsencode(filename), glyph_ids)
1097-
except RuntimeError:
1098-
_log.warning("The PDF backend does not currently support the "
1099-
"selected font.")
1100-
raise
1135+
# Make the charprocs array.
1136+
rawcharprocs = _get_pdf_charprocs(filename, glyph_ids)
11011137
charprocs = {}
11021138
for charname in sorted(rawcharprocs):
11031139
stream = rawcharprocs[charname]

lib/matplotlib/tests/test_mathtext.py

+1
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ def baseline_images(request, fontset, index):
185185
@image_comparison(baseline_images=None)
186186
def test_mathtext_rendering(baseline_images, fontset, index, test):
187187
mpl.rcParams['mathtext.fontset'] = fontset
188+
mpl.rcParams['pdf.compression'] = False
188189
fig = plt.figure(figsize=(5.25, 0.75))
189190
fig.text(0.5, 0.5, test,
190191
horizontalalignment='center', verticalalignment='center')

src/_path.h

+29-25
Original file line numberDiff line numberDiff line change
@@ -1089,35 +1089,39 @@ void quad2cubic(double x0, double y0,
10891089
void __add_number(double val, char format_code, int precision,
10901090
std::string& buffer)
10911091
{
1092-
char *str = PyOS_double_to_string(val, format_code, precision, 0, NULL);
1093-
1094-
// Delete trailing zeros and decimal point
1095-
char *q = str;
1096-
for (; *q != 0; ++q) {
1092+
if (precision == -1) {
1093+
// Special-case for compat with old ttconv code, which *truncated*
1094+
// values with a cast to int instead of rounding them as printf
1095+
// would do. The only point where non-integer values arise is from
1096+
// quad2cubic conversion (as we already perform a first truncation
1097+
// on Python's side), which can introduce additional floating point
1098+
// error (by adding 2/3 delta-x and then 1/3 delta-x), so compensate by
1099+
// first rounding to the closest 1/3 and then truncating.
1100+
char str[255];
1101+
PyOS_snprintf(str, 255, "%d", (int)(round(val * 3)) / 3);
1102+
buffer += str;
1103+
} else {
1104+
char *str = PyOS_double_to_string(val, format_code, precision, 0, NULL);
1105+
// Delete trailing zeros and decimal point
1106+
char *q = str;
10971107
// Find the end of the string
1098-
}
1099-
1100-
--q;
1101-
for (; q >= str && *q == '0'; --q) {
1102-
// Rewind through all the zeros
1103-
}
1104-
1105-
// If the end is a decimal point, delete that too
1106-
if (q >= str && *q == '.') {
1108+
while (*q) { ++q; }
11071109
--q;
1108-
}
1109-
1110-
// Truncate the string
1111-
++q;
1112-
*q = 0;
1113-
1114-
try {
1115-
buffer += str;
1116-
} catch (std::bad_alloc& e) {
1110+
// Rewind through all the zeros
1111+
while (q >= str && *q == '0') { --q; }
1112+
// If the end is a decimal point, delete that too
1113+
if (q >= str && *q == '.') { --q; }
1114+
// Truncate the string
1115+
++q;
1116+
*q = 0;
1117+
try {
1118+
buffer += str;
1119+
} catch (std::bad_alloc& e) {
1120+
PyMem_Free(str);
1121+
throw e;
1122+
}
11171123
PyMem_Free(str);
1118-
throw e;
11191124
}
1120-
PyMem_Free(str);
11211125
}
11221126

11231127

0 commit comments

Comments
 (0)