Skip to content

Commit 1da1a4c

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. figure_align_label.pdf gets modified because of tiny differences in outline extraction (due to the ttconv emulation); there's already the svg test that tests the fixed-dpi case so I feel OK deleting it.
1 parent b12ff2b commit 1da1a4c

File tree

4 files changed

+78
-36
lines changed

4 files changed

+78
-36
lines changed

lib/matplotlib/backends/backend_pdf.py

+48-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,52 @@ 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. Note that occasionally (e.g. with DejaVu Sans
576+
# glyph "0") a point detected as "implicit" is actually explicit, and
577+
# will thus be shifted by 1.
578+
quads, = np.nonzero(c == 3)
579+
quads_on = quads[1::2]
580+
quads_mid_on = np.array(
581+
sorted({*quads_on} & {*quads - 1} & {*quads + 1}), int)
582+
implicit = quads_mid_on[
583+
(v[quads_mid_on] # As above, use astype(int), not // division
584+
== ((v[quads_mid_on - 1] + v[quads_mid_on + 1]) / 2).astype(int))
585+
.all(axis=1)]
586+
if (font.postscript_name, glyph_id) in [
587+
("DejaVuSerif-Italic", 77), # j
588+
("DejaVuSerif-Italic", 135), # \AA
589+
]:
590+
v[:, 0] -= 1 # Hard-coded backcompat (FreeType shifts glyph by 1).
591+
v = (v * conv + .5).astype(int) # As above re: truncation vs rounding.
592+
v[implicit] = (( # Fix implicit points; again, truncate.
593+
(v[implicit - 1] + v[implicit + 1]) / 2).astype(int))
594+
procs[font.get_glyph_name(glyph_id)] = (
595+
" ".join(map(str, d1)).encode("ascii") + b" d1\n"
596+
+ _path.convert_to_string(
597+
Path(v, c), None, None, False, None, -1,
598+
# no code for quad Beziers triggers auto-conversion to cubics.
599+
[b"m", b"l", b"", b"c", b"h"], True)
600+
+ b"f")
601+
return procs
602+
603+
559604
class PdfFile:
560605
"""PDF file object."""
561606

@@ -1089,15 +1134,8 @@ def get_char_width(charcode):
10891134
differencesArray.append(Name(name))
10901135
last_c = c
10911136

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
1137+
# Make the charprocs array.
1138+
rawcharprocs = _get_pdf_charprocs(filename, glyph_ids)
11011139
charprocs = {}
11021140
for charname in sorted(rawcharprocs):
11031141
stream = rawcharprocs[charname]
Binary file not shown.

lib/matplotlib/tests/test_figure.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
import pytest
1919

2020

21-
@image_comparison(['figure_align_labels'],
21+
@image_comparison(['figure_align_labels'], extensions=['png', 'svg'],
2222
tol=0 if platform.machine() == 'x86_64' else 0.01)
2323
def test_align_labels():
2424
fig = plt.figure(tight_layout=True)

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)