From 76b3b6e20b2f7b66a5b7de16cee9a90423a225c0 Mon Sep 17 00:00:00 2001 From: Frank Sauerburger Date: Fri, 9 Jul 2021 15:56:25 +0200 Subject: [PATCH 1/2] Test for kerning in PDF output with type 42 font This commit adds an image comparison test to ensure that the text in a type 42 font has proper kerning in the PDF output. --- .../test_text/text_pdf_font42_kerning.pdf | Bin 0 -> 5364 bytes lib/matplotlib/tests/test_text.py | 7 +++++++ 2 files changed, 7 insertions(+) create mode 100644 lib/matplotlib/tests/baseline_images/test_text/text_pdf_font42_kerning.pdf diff --git a/lib/matplotlib/tests/baseline_images/test_text/text_pdf_font42_kerning.pdf b/lib/matplotlib/tests/baseline_images/test_text/text_pdf_font42_kerning.pdf new file mode 100644 index 0000000000000000000000000000000000000000..a8ce9fca346c69d372dbbc0f2a0f4b60fdafa6da GIT binary patch literal 5364 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 Vgv?|wv>l^@QHDuNYn$l6{s+5*s~Z3S literal 0 HcmV?d00001 diff --git a/lib/matplotlib/tests/test_text.py b/lib/matplotlib/tests/test_text.py index abefe3c3ab04..8c575a7be1a6 100644 --- a/lib/matplotlib/tests/test_text.py +++ b/lib/matplotlib/tests/test_text.py @@ -741,3 +741,10 @@ def test_parse_math(): ax.text(0, 0, r"$ \wrong{math} $", parse_math=True) with pytest.raises(ValueError, match='Unknown symbol'): fig.canvas.draw() + + +@image_comparison(['text_pdf_font42_kerning.pdf'], style='mpl20') +def test_pdf_font42_kerning(): + plt.rcParams['pdf.fonttype'] = 42 + plt.figure() + plt.figtext(0.1, 0.5, "ATAVATAVATAVATAVATA", size=30) From 67aea087db15623c888e40b34d6b53b04872f51c Mon Sep 17 00:00:00 2001 From: Frank Sauerburger Date: Fri, 9 Jul 2021 16:27:36 +0200 Subject: [PATCH 2/2] Emit Type 42 text with TJ and kerning in PDF The commit applies the same kerning algorithm used for Type 3 fonts also to Type 42 fonts. A string is split into chunks with kerning between the chunks. --- lib/matplotlib/backends/backend_pdf.py | 45 +++++++++++++++++--------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py index 10063bd9a7b3..d4cde3155af4 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -2236,6 +2236,20 @@ def encode_string(self, s, fonttype): return s.encode('cp1252', 'replace') return s.encode('utf-16be', 'replace') + @staticmethod + def _font_supports_char(fonttype, char): + """ + Returns True if the font is able to provided the char in a PDF + + For a Type 3 font, this method returns True only for single-byte + chars. For Type 42 fonts this method always returns True. + """ + if fonttype == 3: + return ord(char) <= 255 + if fonttype == 42: + return True + raise NotImplementedError() + def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): # docstring inherited @@ -2270,26 +2284,27 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): } self.file._annotations[-1][1].append(link_annotation) - # If fonttype != 3 emit the whole string at once without manual - # kerning. - if fonttype != 3: + # If fonttype is neither 3 nor 42, emit the whole string at once + # without manual kerning. + if fonttype not in [3, 42]: self.file.output(Op.begin_text, self.file.fontName(prop), fontsize, Op.selectfont) self._setup_textpos(x, y, angle) self.file.output(self.encode_string(s, fonttype), Op.show, Op.end_text) - # There is no way to access multibyte characters of Type 3 fonts, as - # they cannot have a CIDMap. Therefore, in this case we break the - # string into chunks, where each chunk contains either a string of - # consecutive 1-byte characters or a single multibyte character. - # A sequence of 1-byte characters is broken into multiple chunks to - # adjust the kerning between adjacent chunks. Each chunk is emitted - # with a separate command: 1-byte characters use the regular text show - # command (TJ) with appropriate kerning between chunks, whereas - # multibyte characters use the XObject command (Do). (If using Type - # 42 fonts, all of this complication is avoided, but of course, - # subsetting those fonts is complex/hard to implement.) + # A sequence of characters is broken into multiple chunks. The chunking + # serves two purposes: + # - For Type 3 fonts, there is no way to access multibyte characters, + # as they cannot have a CIDMap. Therefore, in this case we break + # the string into chunks, where each chunk contains either a string + # of consecutive 1-byte characters or a single multibyte character. + # - A sequence of 1-byte characters is split into chunks to allow for + # kerning adjustments between consecutive chunks. + # + # Each chunk is emitted with a separate command: 1-byte characters use + # the regular text show command (TJ) with appropriate kerning between + # chunks, whereas multibyte characters use the XObject command (Do). else: # List of (start_x, [prev_kern, char, char, ...]), w/o zero kerns. singlebyte_chunks = [] @@ -2298,7 +2313,7 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): prev_was_multibyte = True for item in _text_helpers.layout( s, font, kern_mode=KERNING_UNFITTED): - if ord(item.char) <= 255: + if self._font_supports_char(fonttype, item.char): if prev_was_multibyte: singlebyte_chunks.append((item.x, [])) if item.prev_kern: