From 3d443e56182f2e84f613a6d4914837a280260829 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jouni=20K=2E=20Sepp=C3=A4nen?= Date: Tue, 13 Jul 2021 20:22:58 +0300 Subject: [PATCH 1/2] Improve the Type-1 font parsing Move Type1Font._tokens into a top-level function _tokenize that is a coroutine. The parsing stage consuming the tokens can instruct the tokenizer to return a binary token - this is necessary when decrypting the CharStrings and Subrs arrays, since the preceding context determines which parts of the data need to be decrypted. The function now also parses the encrypted portion of the font file. To support usage as a coroutine, move the whitespace filtering into the function, since passing the information about binary tokens would not easily work through a filter. The function now returns tokens as subclasses of a new _Token class, which carry the position and value of the token and can have token-specific helper methods. The position data will be needed when modifying the file, as the font is transformed or subsetted. A new helper function _expression can be used to consume tokens that form a balanced subexpression delimited by [] or {}. This helps fix a bug in UniqueID removal: if the font includes PostScript code that checks if the UniqueID is set in the current dictionary, the previous code broke that code instead of removing the UniqueID definition. Fonts can include UniqueID in the encrypted portion as well as the cleartext one, and removal is now done in both portions. Fix a bug related to font weight: the key is title-cased and not lower-cased, so font.prop['weight'] should not exist. --- LICENSE/LICENSE_COURIERTEN | 18 + .../next_api_changes/behavior/20715-JKS.rst | 8 + .../tests/Courier10PitchBT-Bold.pfb | Bin 0 -> 38080 bytes lib/matplotlib/tests/test_type1font.py | 99 ++- lib/matplotlib/type1font.py | 800 ++++++++++++++---- 5 files changed, 744 insertions(+), 181 deletions(-) create mode 100644 LICENSE/LICENSE_COURIERTEN create mode 100644 doc/api/next_api_changes/behavior/20715-JKS.rst create mode 100644 lib/matplotlib/tests/Courier10PitchBT-Bold.pfb diff --git a/LICENSE/LICENSE_COURIERTEN b/LICENSE/LICENSE_COURIERTEN new file mode 100644 index 000000000000..c6d3fd7410a2 --- /dev/null +++ b/LICENSE/LICENSE_COURIERTEN @@ -0,0 +1,18 @@ +The Courier10PitchBT-Bold.pfb file is a Type-1 version of +Courier 10 Pitch BT Bold by Bitstream, obtained from +. It is included +here as test data only, but the following license applies. + + +(c) Copyright 1989-1992, Bitstream Inc., Cambridge, MA. + +You are hereby granted permission under all Bitstream propriety rights +to use, copy, modify, sublicense, sell, and redistribute the 4 Bitstream +Charter (r) Type 1 outline fonts and the 4 Courier Type 1 outline fonts +for any purpose and without restriction; provided, that this notice is +left intact on all copies of such fonts and that Bitstream's trademark +is acknowledged as shown below on all unmodified copies of the 4 Charter +Type 1 fonts. + +BITSTREAM CHARTER is a registered trademark of Bitstream Inc. + diff --git a/doc/api/next_api_changes/behavior/20715-JKS.rst b/doc/api/next_api_changes/behavior/20715-JKS.rst new file mode 100644 index 000000000000..f0ca1d707d3d --- /dev/null +++ b/doc/api/next_api_changes/behavior/20715-JKS.rst @@ -0,0 +1,8 @@ +``Type1Font`` objects include more properties +--------------------------------------------- + +The `.type1font.Type1Font.prop` dictionary now includes more keys, such +as ``CharStrings`` and ``Subrs``. The value of the ``Encoding`` key is +now a dictionary mapping codes to glyph names. The +`.type1font.Type1Font.transform` method now correctly removes +``UniqueID`` properties from the font. diff --git a/lib/matplotlib/tests/Courier10PitchBT-Bold.pfb b/lib/matplotlib/tests/Courier10PitchBT-Bold.pfb new file mode 100644 index 0000000000000000000000000000000000000000..88d9af2af701fb5304b37d87fdb6ca7efe5e8277 GIT binary patch literal 38080 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 RwS|Y5tFwiNBTg{-e*j{lGr<4= literal 0 HcmV?d00001 diff --git a/lib/matplotlib/tests/test_type1font.py b/lib/matplotlib/tests/test_type1font.py index 99cc3e500b0e..39279c229cca 100644 --- a/lib/matplotlib/tests/test_type1font.py +++ b/lib/matplotlib/tests/test_type1font.py @@ -1,6 +1,7 @@ import matplotlib.type1font as t1f import os.path import difflib +import pytest def test_Type1Font(): @@ -13,10 +14,34 @@ def test_Type1Font(): assert font.parts[0] == rawdata[0x0006:0x10c5] assert font.parts[1] == rawdata[0x10cb:0x897f] assert font.parts[2] == rawdata[0x8985:0x8ba6] - assert font.parts[1:] == slanted.parts[1:] - assert font.parts[1:] == condensed.parts[1:] assert font.decrypted.startswith(b'dup\n/Private 18 dict dup begin') assert font.decrypted.endswith(b'mark currentfile closefile\n') + assert slanted.decrypted.startswith(b'dup\n/Private 18 dict dup begin') + assert slanted.decrypted.endswith(b'mark currentfile closefile\n') + assert b'UniqueID 5000793' in font.parts[0] + assert b'UniqueID 5000793' in font.decrypted + assert font._pos['UniqueID'] == [(797, 818), (4483, 4504)] + + len0 = len(font.parts[0]) + for key in font._pos.keys(): + for pos0, pos1 in font._pos[key]: + if pos0 < len0: + data = font.parts[0][pos0:pos1] + else: + data = font.decrypted[pos0-len0:pos1-len0] + assert data.startswith(f'/{key}'.encode('ascii')) + assert {'FontType', 'FontMatrix', 'PaintType', 'ItalicAngle', 'RD' + } < set(font._pos.keys()) + + assert b'UniqueID 5000793' not in slanted.parts[0] + assert b'UniqueID 5000793' not in slanted.decrypted + assert 'UniqueID' not in slanted._pos + assert font.prop['Weight'] == 'Medium' + assert not font.prop['isFixedPitch'] + assert font.prop['ItalicAngle'] == 0 + assert slanted.prop['ItalicAngle'] == -45 + assert font.prop['Encoding'][5] == 'Pi' + assert isinstance(font.prop['CharStrings']['Pi'], bytes) differ = difflib.Differ() diff = list(differ.compare( @@ -24,14 +49,13 @@ def test_Type1Font(): slanted.parts[0].decode('latin-1').splitlines())) for line in ( # Removes UniqueID - '- FontDirectory/CMR10 known{/CMR10 findfont dup/UniqueID known{dup', - '+ FontDirectory/CMR10 known{/CMR10 findfont dup', + '- /UniqueID 5000793 def', # Changes the font name '- /FontName /CMR10 def', - '+ /FontName /CMR10_Slant_1000 def', + '+ /FontName/CMR10_Slant_1000 def', # Alters FontMatrix '- /FontMatrix [0.001 0 0 0.001 0 0 ]readonly def', - '+ /FontMatrix [0.001 0 0.001 0.001 0 0]readonly def', + '+ /FontMatrix [0.001 0 0.001 0.001 0 0] readonly def', # Alters ItalicAngle '- /ItalicAngle 0 def', '+ /ItalicAngle -45.0 def'): @@ -42,17 +66,72 @@ def test_Type1Font(): condensed.parts[0].decode('latin-1').splitlines())) for line in ( # Removes UniqueID - '- FontDirectory/CMR10 known{/CMR10 findfont dup/UniqueID known{dup', - '+ FontDirectory/CMR10 known{/CMR10 findfont dup', + '- /UniqueID 5000793 def', # Changes the font name '- /FontName /CMR10 def', - '+ /FontName /CMR10_Extend_500 def', + '+ /FontName/CMR10_Extend_500 def', # Alters FontMatrix '- /FontMatrix [0.001 0 0 0.001 0 0 ]readonly def', - '+ /FontMatrix [0.0005 0 0 0.001 0 0]readonly def'): + '+ /FontMatrix [0.0005 0 0 0.001 0 0] readonly def'): assert line in diff, 'diff to condensed font must contain %s' % line +def test_Type1Font_2(): + filename = os.path.join(os.path.dirname(__file__), + 'Courier10PitchBT-Bold.pfb') + font = t1f.Type1Font(filename) + assert font.prop['Weight'] == 'Bold' + assert font.prop['isFixedPitch'] + assert font.prop['Encoding'][65] == 'A' # the font uses StandardEncoding + (pos0, pos1), = font._pos['Encoding'] + assert font.parts[0][pos0:pos1] == b'/Encoding StandardEncoding' + + +def test_tokenize(): + data = (b'1234/abc false -9.81 Foo <<[0 1 2]<0 1ef a\t>>>\n' + b'(string with(nested\t\\) par)ens\\\\)') + # 1 2 x 2 xx1 + # 1 and 2 are matching parens, x means escaped character + n, w, num, kw, d = 'name', 'whitespace', 'number', 'keyword', 'delimiter' + b, s = 'boolean', 'string' + correct = [ + (num, 1234), (n, 'abc'), (w, ' '), (b, False), (w, ' '), (num, -9.81), + (w, ' '), (kw, 'Foo'), (w, ' '), (d, '<<'), (d, '['), (num, 0), + (w, ' '), (num, 1), (w, ' '), (num, 2), (d, ']'), (s, b'\x01\xef\xa0'), + (d, '>>'), (w, '\n'), (s, 'string with(nested\t) par)ens\\') + ] + correct_no_ws = [x for x in correct if x[0] != w] + + def convert(tokens): + return [(t.kind, t.value()) for t in tokens] + + assert convert(t1f._tokenize(data, False)) == correct + assert convert(t1f._tokenize(data, True)) == correct_no_ws + + def bin_after(n): + tokens = t1f._tokenize(data, True) + result = [] + for _ in range(n): + result.append(next(tokens)) + result.append(tokens.send(10)) + return convert(result) + + for n in range(1, len(correct_no_ws)): + result = bin_after(n) + assert result[:-1] == correct_no_ws[:n] + assert result[-1][0] == 'binary' + assert isinstance(result[-1][1], bytes) + + +def test_tokenize_errors(): + with pytest.raises(ValueError): + list(t1f._tokenize(b'1234 (this (string) is unterminated\\)', True)) + with pytest.raises(ValueError): + list(t1f._tokenize(b'/Foo<01234', True)) + with pytest.raises(ValueError): + list(t1f._tokenize(b'/Foo<01234abcg>/Bar', True)) + + def test_overprecision(): # We used to output too many digits in FontMatrix entries and # ItalicAngle, which could make Type-1 parsers unhappy. diff --git a/lib/matplotlib/type1font.py b/lib/matplotlib/type1font.py index f417c0fc97a4..871f13642ee3 100644 --- a/lib/matplotlib/type1font.py +++ b/lib/matplotlib/type1font.py @@ -22,10 +22,10 @@ """ import binascii -import enum -import itertools +import functools import logging import re +import string import struct import numpy as np @@ -35,9 +35,292 @@ _log = logging.getLogger(__name__) -# token types -_TokenType = enum.Enum('_TokenType', - 'whitespace name string delimiter number') + +class _Token: + """ + A token in a PostScript stream + + Attributes + ---------- + pos : int + position, i.e. offset from the beginning of the data + + raw : str + the raw text of the token + + kind : str + description of the token (for debugging or testing) + """ + __slots__ = ('pos', 'raw') + kind = '?' + + def __init__(self, pos, raw): + _log.debug('type1font._Token %s at %d: %r', self.kind, pos, raw) + self.pos = pos + self.raw = raw + + def __str__(self): + return f"<{self.kind} {self.raw} @{self.pos}>" + + def endpos(self): + """Position one past the end of the token""" + return self.pos + len(self.raw) + + def is_keyword(self, *names): + """Is this a name token with one of the names?""" + return False + + def is_slash_name(self): + """Is this a name token that starts with a slash?""" + return False + + def is_delim(self): + """Is this a delimiter token?""" + return False + + def is_number(self): + """Is this a number token?""" + return False + + def value(self): + return self.raw + + +class _NameToken(_Token): + kind = 'name' + + def is_slash_name(self): + return self.raw.startswith('/') + + def value(self): + return self.raw[1:] + + +class _BooleanToken(_Token): + kind = 'boolean' + + def value(self): + return self.raw == 'true' + + +class _KeywordToken(_Token): + kind = 'keyword' + + def is_keyword(self, *names): + return self.raw in names + + +class _DelimiterToken(_Token): + kind = 'delimiter' + + def is_delim(self): + return True + + def opposite(self): + return {'[': ']', ']': '[', + '{': '}', '}': '{', + '<<': '>>', '>>': '<<' + }[self.raw] + + +class _WhitespaceToken(_Token): + kind = 'whitespace' + + +class _StringToken(_Token): + kind = 'string' + _escapes_re = re.compile(r'\\([\\()nrtbf]|[0-7]{1,3})') + _replacements = {'\\': '\\', '(': '(', ')': ')', 'n': '\n', + 'r': '\r', 't': '\t', 'b': '\b', 'f': '\f'} + _ws_re = re.compile('[\0\t\r\f\n ]') + + @classmethod + def _escape(cls, match): + group = match.group(1) + try: + return cls._replacements[group] + except KeyError: + return chr(int(group, 8)) + + @functools.lru_cache() + def value(self): + if self.raw[0] == '(': + return self._escapes_re.sub(self._escape, self.raw[1:-1]) + else: + data = self._ws_re.sub('', self.raw[1:-1]) + if len(data) % 2 == 1: + data += '0' + return binascii.unhexlify(data) + + +class _BinaryToken(_Token): + kind = 'binary' + + def value(self): + return self.raw[1:] + + +class _NumberToken(_Token): + kind = 'number' + + def is_number(self): + return True + + def value(self): + if '.' not in self.raw: + return int(self.raw) + else: + return float(self.raw) + + +def _tokenize(data: bytes, skip_ws: bool): + """ + A generator that produces _Token instances from Type-1 font code. + + The consumer of the generator may send an integer to the tokenizer + to indicate that the next token should be _BinaryToken of the given + length. + + Parameters + ---------- + data : bytes + The data of the font to tokenize. + + skip_ws : bool + If true, the generator will drop any _WhitespaceTokens from the output. + """ + + text = data.decode('ascii', 'replace') + whitespace_or_comment_re = re.compile(r'[\0\t\r\f\n ]+|%[^\r\n]*') + token_re = re.compile(r'/{0,2}[^]\0\t\r\f\n ()<>{}/%[]+') + instring_re = re.compile(r'[()\\]') + hex_re = re.compile(r'^<[0-9a-fA-F\0\t\r\f\n ]*>$') + oct_re = re.compile(r'[0-7]{1,3}') + pos = 0 + next_binary = None + + while pos < len(text): + if next_binary is not None: + n = next_binary + next_binary = (yield _BinaryToken(pos, data[pos:pos+n])) + pos += n + continue + match = whitespace_or_comment_re.match(text, pos) + if match: + if not skip_ws: + next_binary = (yield _WhitespaceToken(pos, match.group())) + pos = match.end() + elif text[pos] == '(': + # PostScript string rules: + # - parentheses must be balanced + # - backslashes escape backslashes and parens + # - also codes \n\r\t\b\f and octal escapes are recognized + # - other backslashes do not escape anything + start = pos + pos += 1 + depth = 1 + while depth: + match = instring_re.search(text, pos) + if match is None: + raise ValueError( + f'Unterminated string starting at {start}') + pos = match.end() + if match.group() == '(': + depth += 1 + elif match.group() == ')': + depth -= 1 + else: # a backslash + char = text[pos] + if char in r'\()nrtbf': + pos += 1 + else: + octal = oct_re.match(text, pos) + if octal: + pos = octal.end() + else: + pass # non-escaping backslash + next_binary = (yield _StringToken(start, text[start:pos])) + elif text[pos:pos + 2] in ('<<', '>>'): + next_binary = (yield _DelimiterToken(pos, text[pos:pos + 2])) + pos += 2 + elif text[pos] == '<': + start = pos + try: + pos = text.index('>', pos) + 1 + except ValueError as e: + raise ValueError(f'Unterminated hex string starting at {start}' + ) from e + if not hex_re.match(text[start:pos]): + raise ValueError(f'Malformed hex string starting at {start}') + next_binary = (yield _StringToken(pos, text[start:pos])) + else: + match = token_re.match(text, pos) + if match: + raw = match.group() + if raw.startswith('/'): + next_binary = (yield _NameToken(pos, raw)) + elif match.group() in ('true', 'false'): + next_binary = (yield _BooleanToken(pos, raw)) + else: + try: + float(raw) + next_binary = (yield _NumberToken(pos, raw)) + except ValueError: + next_binary = (yield _KeywordToken(pos, raw)) + pos = match.end() + else: + next_binary = (yield _DelimiterToken(pos, text[pos])) + pos += 1 + + +class _BalancedExpression(_Token): + pass + + +def _expression(initial, tokens, data): + """ + Consume some number of tokens and return a balanced PostScript expression + + Parameters + ---------- + initial : _Token + the token that triggered parsing a balanced expression + + tokens : iterator of _Token + following tokens + + data : bytes + underlying data that the token positions point to + + Returns + ------- + _BalancedExpression + """ + delim_stack = [] + token = initial + while True: + if token.is_delim(): + if token.raw in ('[', '{'): + delim_stack.append(token) + elif token.raw in (']', '}'): + if not delim_stack: + raise RuntimeError(f"unmatched closing token {token}") + match = delim_stack.pop() + if match.raw != token.opposite(): + raise RuntimeError( + f"opening token {match} closed by {token}" + ) + if not delim_stack: + break + else: + raise RuntimeError(f'unknown delimiter {token}') + elif not delim_stack: + break + token = next(tokens) + return _BalancedExpression( + initial.pos, + data[initial.pos:token.endpos()].decode('ascii', 'replace') + ) class Type1Font: @@ -52,9 +335,20 @@ class Type1Font: decrypted : bytes The decrypted form of parts[1]. prop : dict[str, Any] - A dictionary of font properties. + A dictionary of font properties. Noteworthy keys include: + FontName - PostScript name of the font + Encoding - dict from numeric codes to glyph names + FontMatrix - bytes object encoding a matrix + UniqueID - optional font identifier, dropped when modifying the font + CharStrings - dict from glyph names to byte code + Subrs - array of byte code subroutines + OtherSubrs - bytes object encoding some PostScript code """ - __slots__ = ('parts', 'decrypted', 'prop') + __slots__ = ('parts', 'decrypted', 'prop', '_pos') + # the _pos dict contains (begin, end) indices to parts[0] + decrypted + # so that they can be replaced when transforming the font; + # but since sometimes a definition appears in both parts[0] and decrypted, + # _pos[name] is an array of such pairs def __init__(self, input): """ @@ -144,10 +438,6 @@ def _split(self, data): return data[:len1], binary, data[idx+1:] - _whitespace_or_comment_re = re.compile(br'[\0\t\r\014\n ]+|%[^\r\n\v]*') - _token_re = re.compile(br'/{0,2}[^]\0\t\r\v\n ()<>{}/%[]+') - _instring_re = re.compile(br'[()\\]') - @staticmethod def _decrypt(ciphertext, key, ndiscard=4): """ @@ -196,101 +486,75 @@ def _encrypt(plaintext, key, ndiscard=4): return bytes(ciphertext) - @classmethod - def _tokens(cls, text): - """ - A PostScript tokenizer. Yield (token, value) pairs such as - (_TokenType.whitespace, ' ') or (_TokenType.name, '/Foobar'). - """ - # Preload enum members for speed. - tok_whitespace = _TokenType.whitespace - tok_name = _TokenType.name - tok_string = _TokenType.string - tok_delimiter = _TokenType.delimiter - tok_number = _TokenType.number - pos = 0 - while pos < len(text): - match = cls._whitespace_or_comment_re.match(text, pos) - if match: - yield (tok_whitespace, match.group()) - pos = match.end() - elif text[pos:pos+1] == b'(': - start = pos - pos += 1 - depth = 1 - while depth: - match = cls._instring_re.search(text, pos) - if match is None: - return - pos = match.end() - if match.group() == b'(': - depth += 1 - elif match.group() == b')': - depth -= 1 - else: # a backslash - skip the next character - pos += 1 - yield (tok_string, text[start:pos]) - elif text[pos:pos + 2] in (b'<<', b'>>'): - yield (tok_delimiter, text[pos:pos + 2]) - pos += 2 - elif text[pos:pos+1] == b'<': - start = pos - pos = text.index(b'>', pos) - yield (tok_string, text[start:pos]) - else: - match = cls._token_re.match(text, pos) - if match: - try: - float(match.group()) - yield (tok_number, match.group()) - except ValueError: - yield (tok_name, match.group()) - pos = match.end() - else: - yield (tok_delimiter, text[pos:pos + 1]) - pos += 1 - def _parse(self): """ Find the values of various font properties. This limited kind of parsing is described in Chapter 10 "Adobe Type Manager Compatibility" of the Type-1 spec. """ - # Preload enum members for speed. - tok_whitespace = _TokenType.whitespace - tok_name = _TokenType.name - tok_string = _TokenType.string - tok_number = _TokenType.number # Start with reasonable defaults - prop = {'weight': 'Regular', 'ItalicAngle': 0.0, 'isFixedPitch': False, + prop = {'Weight': 'Regular', 'ItalicAngle': 0.0, 'isFixedPitch': False, 'UnderlinePosition': -100, 'UnderlineThickness': 50} - filtered = ((token, value) - for token, value in self._tokens(self.parts[0]) - if token is not tok_whitespace) - # The spec calls this an ASCII format; in Python 2.x we could - # just treat the strings and names as opaque bytes but let's - # turn them into proper Unicode, and be lenient in case of high bytes. - def convert(x): return x.decode('ascii', 'replace') - for token, value in filtered: - if token is tok_name and value.startswith(b'/'): - key = convert(value[1:]) - token, value = next(filtered) - if token is tok_name: - if value in (b'true', b'false'): - value = value == b'true' - else: - value = convert(value.lstrip(b'/')) - elif token is tok_string: - value = convert(value.lstrip(b'(').rstrip(b')')) - elif token is tok_number: - if b'.' in value: - value = float(value) - else: - value = int(value) - else: # more complicated value such as an array - value = None - if key != 'FontInfo' and value is not None: - prop[key] = value + pos = {} + data = self.parts[0] + self.decrypted + + source = _tokenize(data, True) + while True: + # See if there is a key to be assigned a value + # e.g. /FontName in /FontName /Helvetica def + try: + token = next(source) + except StopIteration: + break + if token.is_delim(): + # skip over this - we want top-level keys only + _expression(token, source, data) + if token.is_slash_name(): + key = token.value() + keypos = token.pos + else: + continue + + # Some values need special parsing + if key in ('Subrs', 'CharStrings', 'Encoding', 'OtherSubrs'): + prop[key], endpos = { + 'Subrs': self._parse_subrs, + 'CharStrings': self._parse_charstrings, + 'Encoding': self._parse_encoding, + 'OtherSubrs': self._parse_othersubrs + }[key](source, data) + pos.setdefault(key, []).append((keypos, endpos)) + continue + + try: + token = next(source) + except StopIteration: + break + + if isinstance(token, _KeywordToken): + # constructs like + # FontDirectory /Helvetica known {...} {...} ifelse + # mean the key was not really a key + continue + + if token.is_delim(): + value = _expression(token, source, data).raw + else: + value = token.value() + + # look for a 'def' possibly preceded by access modifiers + try: + kw = next( + kw for kw in source + if not kw.is_keyword('readonly', 'noaccess', 'executeonly') + ) + except StopIteration: + break + + # sometimes noaccess def and readonly def are abbreviated + if kw.is_name(b'def', b'ND', b'RD', b'|-'): + prop[key] = value + pos.setdefault(key, []).append((keypos, kw.endpos())) # Fill in the various *Name properties if 'FontName' not in prop: @@ -303,79 +567,114 @@ def convert(x): return x.decode('ascii', 'replace') extras = ('(?i)([ -](regular|plain|italic|oblique|(semi)?bold|' '(ultra)?light|extra|condensed))+$') prop['FamilyName'] = re.sub(extras, '', prop['FullName']) + # Decrypt the encrypted parts + ndiscard = prop.get('lenIV', 4) + cs = prop['CharStrings'] + for key, value in cs.items(): + cs[key] = self._decrypt(value, 'charstring', ndiscard) + if 'Subrs' in prop: + prop['Subrs'] = [ + self._decrypt(value, 'charstring', ndiscard) + for value in prop['Subrs'] + ] self.prop = prop + self._pos = pos - @classmethod - def _transformer(cls, tokens, slant, extend): - tok_whitespace = _TokenType.whitespace - tok_name = _TokenType.name - - def fontname(name): - result = name - if slant: - result += b'_Slant_%d' % int(1000 * slant) - if extend != 1.0: - result += b'_Extend_%d' % int(1000 * extend) - return result - - def italicangle(angle): - return b'%a' % round( - float(angle) - np.arctan(slant) / np.pi * 180, - 5 + def _parse_subrs(self, tokens, _data): + count_token = next(tokens) + if not count_token.is_number(): + raise RuntimeError( + f"Token following /Subrs must be a number, was {count_token}" ) + count = count_token.value() + array = [None] * count + next(t for t in tokens if t.is_keyword('array')) + for _ in range(count): + next(t for t in tokens if t.is_keyword('dup')) + index_token = next(tokens) + if not index_token.is_number(): + raise RuntimeError( + "Token following dup in Subrs definition must be a " + f"number, was {index_token}" + ) + nbytes_token = next(tokens) + if not nbytes_token.is_number(): + raise RuntimeError( + "Second token following dup in Subrs definition must " + f"be a number, was {nbytes_token}" + ) + token = next(tokens) # usually RD or |- but the font can define this to be anything + binary_token = tokens.send(1+nbytes_token.numeric_value()) + array[index_token.numeric_value()] = binary_token.value[1:] + + return array, next(tokens).endpos() - def fontmatrix(array): - array = array.lstrip(b'[').rstrip(b']').split() - array = [float(x) for x in array] - oldmatrix = np.eye(3, 3) - oldmatrix[0:3, 0] = array[::2] - oldmatrix[0:3, 1] = array[1::2] - modifier = np.array([[extend, 0, 0], - [slant, 1, 0], - [0, 0, 1]]) - newmatrix = np.dot(modifier, oldmatrix) - array[::2] = newmatrix[0:3, 0] - array[1::2] = newmatrix[0:3, 1] - return ( - '[%s]' % ' '.join(_format_approx(x, 6) for x in array) - ).encode('ascii') - - def replace(fun): - def replacer(tokens): - token, value = next(tokens) # name, e.g., /FontMatrix - yield value - token, value = next(tokens) # possible whitespace - while token is tok_whitespace: - yield value - token, value = next(tokens) - if value != b'[': # name/number/etc. - yield fun(value) - else: # array, e.g., [1 2 3] - result = b'' - while value != b']': - result += value - token, value = next(tokens) - result += value - yield fun(result) - return replacer - - def suppress(tokens): - for _ in itertools.takewhile(lambda x: x[1] != b'def', tokens): - pass - yield b'' - - table = {b'/FontName': replace(fontname), - b'/ItalicAngle': replace(italicangle), - b'/FontMatrix': replace(fontmatrix), - b'/UniqueID': suppress} - - for token, value in tokens: - if token is tok_name and value in table: - yield from table[value]( - itertools.chain([(token, value)], tokens)) - else: - yield value + @staticmethod + def _parse_charstrings(tokens, _data): + count_token = next(tokens) + if not count_token.is_number(): + raise RuntimeError( + "Token following /CharStrings must be a number, " + f"was {count_token}" + ) + count = count_token.value() + charstrings = {} + next(t for t in tokens if t.is_keyword('begin')) + while True: + token = next(t for t in tokens + if t.is_keyword('end') or t.is_slash_name()) + if token.raw == 'end': + return charstrings, token.endpos() + glyphname = token.value() + nbytes_token = next(tokens) + if not nbytes_token.is_number(): + raise RuntimeError( + f"Token following /{glyphname} in CharStrings definition " + f"must be a number, was {nbytes_token}" + ) + next(tokens) # usually RD or |- + binary_token = tokens.send(1+nbytes_token.value()) + charstrings[glyphname] = binary_token.value() + + @staticmethod + def _parse_encoding(tokens, _data): + # this only works for encodings that follow the Adobe manual + # but some old fonts include non-compliant data - we log a warning + # and return a possibly incomplete encoding + encoding = {} + while True: + token = next(t for t in tokens + if t.is_keyword('StandardEncoding', 'dup', 'def')) + if token.is_keyword('StandardEncoding'): + return _StandardEncoding, token.endpos() + if token.is_keyword('def'): + return encoding, token.endpos() + index_token = next(tokens) + if not index_token.is_number(): + _log.warning( + f"Parsing encoding: expected number, got {index_token}" + ) + continue + name_token = next(tokens) + if not name_token.is_slash_name(): + _log.warning( + f"Parsing encoding: expected slash-name, got {name_token}" + ) + continue + encoding[index_token.value()] = name_token.value() + + @staticmethod + def _parse_othersubrs(tokens, data): + init_pos = None + while True: + token = next(tokens) + if init_pos is None: + init_pos = token.pos + if token.is_delim(): + _expression(token, tokens, data) + elif token.is_keyword('def', 'ND', '|-'): + return data[init_pos:token.endpos()], token.endpos() def transform(self, effects): """ @@ -397,8 +696,167 @@ def transform(self, effects): ------- `Type1Font` """ - tokenizer = self._tokens(self.parts[0]) - transformed = self._transformer(tokenizer, - slant=effects.get('slant', 0.0), - extend=effects.get('extend', 1.0)) - return Type1Font((b"".join(transformed), self.parts[1], self.parts[2])) + fontname = self.prop['FontName'] + italicangle = self.prop['ItalicAngle'] + + array = [ + float(x) for x in (self.prop['FontMatrix'] + .lstrip('[').rstrip(']').split()) + ] + oldmatrix = np.eye(3, 3) + oldmatrix[0:3, 0] = array[::2] + oldmatrix[0:3, 1] = array[1::2] + modifier = np.eye(3, 3) + + if 'slant' in effects: + slant = effects['slant'] + fontname += '_Slant_%d' % int(1000 * slant) + italicangle = round( + float(italicangle) - np.arctan(slant) / np.pi * 180, + 5 + ) + modifier[1, 0] = slant + + if 'extend' in effects: + extend = effects['extend'] + fontname += '_Extend_%d' % int(1000 * extend) + modifier[0, 0] = extend + + newmatrix = np.dot(modifier, oldmatrix) + array[::2] = newmatrix[0:3, 0] + array[1::2] = newmatrix[0:3, 1] + fontmatrix = ( + '[%s]' % ' '.join(_format_approx(x, 6) for x in array) + ) + replacements = ( + [(x, '/FontName/%s def' % fontname) + for x in self._pos['FontName']] + + [(x, '/ItalicAngle %a def' % italicangle) + for x in self._pos['ItalicAngle']] + + [(x, '/FontMatrix %s readonly def' % fontmatrix) + for x in self._pos['FontMatrix']] + + [(x, '') for x in self._pos.get('UniqueID', [])] + ) + + data = bytearray(self.parts[0]) + data.extend(self.decrypted) + len0 = len(self.parts[0]) + for (pos0, pos1), value in sorted(replacements, reverse=True): + data[pos0:pos1] = value.encode('ascii', 'replace') + if pos0 < len(self.parts[0]): + if pos1 >= len(self.parts[0]): + raise RuntimeError( + f"text to be replaced with {value} spans " + "the eexec boundary" + ) + len0 += len(value) - pos1 + pos0 + + data = bytes(data) + return Type1Font(( + data[:len0], + self._encrypt(data[len0:], 'eexec'), + self.parts[2] + )) + + +_StandardEncoding = { + **{ord(letter): letter for letter in string.ascii_letters}, + 0: '.notdef', + 32: 'space', + 33: 'exclam', + 34: 'quotedbl', + 35: 'numbersign', + 36: 'dollar', + 37: 'percent', + 38: 'ampersand', + 39: 'quoteright', + 40: 'parenleft', + 41: 'parenright', + 42: 'asterisk', + 43: 'plus', + 44: 'comma', + 45: 'hyphen', + 46: 'period', + 47: 'slash', + 48: 'zero', + 49: 'one', + 50: 'two', + 51: 'three', + 52: 'four', + 53: 'five', + 54: 'six', + 55: 'seven', + 56: 'eight', + 57: 'nine', + 58: 'colon', + 59: 'semicolon', + 60: 'less', + 61: 'equal', + 62: 'greater', + 63: 'question', + 64: 'at', + 91: 'bracketleft', + 92: 'backslash', + 93: 'bracketright', + 94: 'asciicircum', + 95: 'underscore', + 96: 'quoteleft', + 123: 'braceleft', + 124: 'bar', + 125: 'braceright', + 126: 'asciitilde', + 161: 'exclamdown', + 162: 'cent', + 163: 'sterling', + 164: 'fraction', + 165: 'yen', + 166: 'florin', + 167: 'section', + 168: 'currency', + 169: 'quotesingle', + 170: 'quotedblleft', + 171: 'guillemotleft', + 172: 'guilsinglleft', + 173: 'guilsinglright', + 174: 'fi', + 175: 'fl', + 177: 'endash', + 178: 'dagger', + 179: 'daggerdbl', + 180: 'periodcentered', + 182: 'paragraph', + 183: 'bullet', + 184: 'quotesinglbase', + 185: 'quotedblbase', + 186: 'quotedblright', + 187: 'guillemotright', + 188: 'ellipsis', + 189: 'perthousand', + 191: 'questiondown', + 193: 'grave', + 194: 'acute', + 195: 'circumflex', + 196: 'tilde', + 197: 'macron', + 198: 'breve', + 199: 'dotaccent', + 200: 'dieresis', + 202: 'ring', + 203: 'cedilla', + 205: 'hungarumlaut', + 206: 'ogonek', + 207: 'caron', + 208: 'emdash', + 225: 'AE', + 227: 'ordfeminine', + 232: 'Lslash', + 233: 'Oslash', + 234: 'OE', + 235: 'ordmasculine', + 241: 'ae', + 245: 'dotlessi', + 248: 'lslash', + 249: 'oslash', + 250: 'oe', + 251: 'germandbls', +} From e98bb83384729684ce4c991448d969f7b016d392 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jouni=20K=2E=20Sepp=C3=A4nen?= Date: Thu, 22 Jul 2021 13:36:51 +0300 Subject: [PATCH 2/2] Recognize abbreviations of PostScript code Type-1 fonts are required to have subroutines with specific contents but their names may vary. They are usually ND, NP and RD but names like | and |- appear too. --- lib/matplotlib/tests/test_type1font.py | 2 ++ lib/matplotlib/type1font.py | 27 +++++++++++++++++++++----- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/lib/matplotlib/tests/test_type1font.py b/lib/matplotlib/tests/test_type1font.py index 39279c229cca..6a16da10def1 100644 --- a/lib/matplotlib/tests/test_type1font.py +++ b/lib/matplotlib/tests/test_type1font.py @@ -42,6 +42,7 @@ def test_Type1Font(): assert slanted.prop['ItalicAngle'] == -45 assert font.prop['Encoding'][5] == 'Pi' assert isinstance(font.prop['CharStrings']['Pi'], bytes) + assert font._abbr['ND'] == 'ND' differ = difflib.Differ() diff = list(differ.compare( @@ -85,6 +86,7 @@ def test_Type1Font_2(): assert font.prop['Encoding'][65] == 'A' # the font uses StandardEncoding (pos0, pos1), = font._pos['Encoding'] assert font.parts[0][pos0:pos1] == b'/Encoding StandardEncoding' + assert font._abbr['ND'] == '|-' def test_tokenize(): diff --git a/lib/matplotlib/type1font.py b/lib/matplotlib/type1font.py index 871f13642ee3..4c39ea8750b9 100644 --- a/lib/matplotlib/type1font.py +++ b/lib/matplotlib/type1font.py @@ -344,11 +344,14 @@ class Type1Font: Subrs - array of byte code subroutines OtherSubrs - bytes object encoding some PostScript code """ - __slots__ = ('parts', 'decrypted', 'prop', '_pos') + __slots__ = ('parts', 'decrypted', 'prop', '_pos', '_abbr') # the _pos dict contains (begin, end) indices to parts[0] + decrypted # so that they can be replaced when transforming the font; # but since sometimes a definition appears in both parts[0] and decrypted, # _pos[name] is an array of such pairs + # + # _abbr maps three standard abbreviations to their particular names in + # this font (e.g. 'RD' is named '-|' in some fonts) def __init__(self, input): """ @@ -368,6 +371,7 @@ def __init__(self, input): self.parts = self._split(data) self.decrypted = self._decrypt(self.parts[1], 'eexec') + self._abbr = {'RD': 'RD', 'ND': 'ND', 'NP': 'NP'} self._parse() def _read(self, file): @@ -552,10 +556,18 @@ def _parse(self): break # sometimes noaccess def and readonly def are abbreviated - if kw.is_name(b'def', b'ND', b'RD', b'|-'): + if kw.is_keyword('def', self._abbr['ND'], self._abbr['NP']): prop[key] = value pos.setdefault(key, []).append((keypos, kw.endpos())) + # detect the standard abbreviations + if value == '{noaccess def}': + self._abbr['ND'] = key + elif value == '{noaccess put}': + self._abbr['NP'] = key + elif value == '{string currentfile exch readstring pop}': + self._abbr['RD'] = key + # Fill in the various *Name properties if 'FontName' not in prop: prop['FontName'] = (prop.get('FullName') or @@ -604,9 +616,14 @@ def _parse_subrs(self, tokens, _data): "Second token following dup in Subrs definition must " f"be a number, was {nbytes_token}" ) - token = next(tokens) # usually RD or |- but the font can define this to be anything - binary_token = tokens.send(1+nbytes_token.numeric_value()) - array[index_token.numeric_value()] = binary_token.value[1:] + token = next(tokens) + if not token.is_keyword(self._abbr['RD']): + raise RuntimeError( + f"Token preceding subr must be {self._abbr['RD']}, " + f"was {token}" + ) + binary_token = tokens.send(1+nbytes_token.value()) + array[index_token.value()] = binary_token.value() return array, next(tokens).endpos()