Skip to content

Commit 3d443e5

Browse files
committed
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.
1 parent 276a9f3 commit 3d443e5

File tree

5 files changed

+744
-181
lines changed

5 files changed

+744
-181
lines changed

LICENSE/LICENSE_COURIERTEN

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
The Courier10PitchBT-Bold.pfb file is a Type-1 version of
2+
Courier 10 Pitch BT Bold by Bitstream, obtained from
3+
<https://ctan.org/tex-archive/fonts/courierten>. It is included
4+
here as test data only, but the following license applies.
5+
6+
7+
(c) Copyright 1989-1992, Bitstream Inc., Cambridge, MA.
8+
9+
You are hereby granted permission under all Bitstream propriety rights
10+
to use, copy, modify, sublicense, sell, and redistribute the 4 Bitstream
11+
Charter (r) Type 1 outline fonts and the 4 Courier Type 1 outline fonts
12+
for any purpose and without restriction; provided, that this notice is
13+
left intact on all copies of such fonts and that Bitstream's trademark
14+
is acknowledged as shown below on all unmodified copies of the 4 Charter
15+
Type 1 fonts.
16+
17+
BITSTREAM CHARTER is a registered trademark of Bitstream Inc.
18+
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
``Type1Font`` objects include more properties
2+
---------------------------------------------
3+
4+
The `.type1font.Type1Font.prop` dictionary now includes more keys, such
5+
as ``CharStrings`` and ``Subrs``. The value of the ``Encoding`` key is
6+
now a dictionary mapping codes to glyph names. The
7+
`.type1font.Type1Font.transform` method now correctly removes
8+
``UniqueID`` properties from the font.
37.2 KB
Binary file not shown.

lib/matplotlib/tests/test_type1font.py

Lines changed: 89 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import matplotlib.type1font as t1f
22
import os.path
33
import difflib
4+
import pytest
45

56

67
def test_Type1Font():
@@ -13,25 +14,48 @@ def test_Type1Font():
1314
assert font.parts[0] == rawdata[0x0006:0x10c5]
1415
assert font.parts[1] == rawdata[0x10cb:0x897f]
1516
assert font.parts[2] == rawdata[0x8985:0x8ba6]
16-
assert font.parts[1:] == slanted.parts[1:]
17-
assert font.parts[1:] == condensed.parts[1:]
1817
assert font.decrypted.startswith(b'dup\n/Private 18 dict dup begin')
1918
assert font.decrypted.endswith(b'mark currentfile closefile\n')
19+
assert slanted.decrypted.startswith(b'dup\n/Private 18 dict dup begin')
20+
assert slanted.decrypted.endswith(b'mark currentfile closefile\n')
21+
assert b'UniqueID 5000793' in font.parts[0]
22+
assert b'UniqueID 5000793' in font.decrypted
23+
assert font._pos['UniqueID'] == [(797, 818), (4483, 4504)]
24+
25+
len0 = len(font.parts[0])
26+
for key in font._pos.keys():
27+
for pos0, pos1 in font._pos[key]:
28+
if pos0 < len0:
29+
data = font.parts[0][pos0:pos1]
30+
else:
31+
data = font.decrypted[pos0-len0:pos1-len0]
32+
assert data.startswith(f'/{key}'.encode('ascii'))
33+
assert {'FontType', 'FontMatrix', 'PaintType', 'ItalicAngle', 'RD'
34+
} < set(font._pos.keys())
35+
36+
assert b'UniqueID 5000793' not in slanted.parts[0]
37+
assert b'UniqueID 5000793' not in slanted.decrypted
38+
assert 'UniqueID' not in slanted._pos
39+
assert font.prop['Weight'] == 'Medium'
40+
assert not font.prop['isFixedPitch']
41+
assert font.prop['ItalicAngle'] == 0
42+
assert slanted.prop['ItalicAngle'] == -45
43+
assert font.prop['Encoding'][5] == 'Pi'
44+
assert isinstance(font.prop['CharStrings']['Pi'], bytes)
2045

2146
differ = difflib.Differ()
2247
diff = list(differ.compare(
2348
font.parts[0].decode('latin-1').splitlines(),
2449
slanted.parts[0].decode('latin-1').splitlines()))
2550
for line in (
2651
# Removes UniqueID
27-
'- FontDirectory/CMR10 known{/CMR10 findfont dup/UniqueID known{dup',
28-
'+ FontDirectory/CMR10 known{/CMR10 findfont dup',
52+
'- /UniqueID 5000793 def',
2953
# Changes the font name
3054
'- /FontName /CMR10 def',
31-
'+ /FontName /CMR10_Slant_1000 def',
55+
'+ /FontName/CMR10_Slant_1000 def',
3256
# Alters FontMatrix
3357
'- /FontMatrix [0.001 0 0 0.001 0 0 ]readonly def',
34-
'+ /FontMatrix [0.001 0 0.001 0.001 0 0]readonly def',
58+
'+ /FontMatrix [0.001 0 0.001 0.001 0 0] readonly def',
3559
# Alters ItalicAngle
3660
'- /ItalicAngle 0 def',
3761
'+ /ItalicAngle -45.0 def'):
@@ -42,17 +66,72 @@ def test_Type1Font():
4266
condensed.parts[0].decode('latin-1').splitlines()))
4367
for line in (
4468
# Removes UniqueID
45-
'- FontDirectory/CMR10 known{/CMR10 findfont dup/UniqueID known{dup',
46-
'+ FontDirectory/CMR10 known{/CMR10 findfont dup',
69+
'- /UniqueID 5000793 def',
4770
# Changes the font name
4871
'- /FontName /CMR10 def',
49-
'+ /FontName /CMR10_Extend_500 def',
72+
'+ /FontName/CMR10_Extend_500 def',
5073
# Alters FontMatrix
5174
'- /FontMatrix [0.001 0 0 0.001 0 0 ]readonly def',
52-
'+ /FontMatrix [0.0005 0 0 0.001 0 0]readonly def'):
75+
'+ /FontMatrix [0.0005 0 0 0.001 0 0] readonly def'):
5376
assert line in diff, 'diff to condensed font must contain %s' % line
5477

5578

79+
def test_Type1Font_2():
80+
filename = os.path.join(os.path.dirname(__file__),
81+
'Courier10PitchBT-Bold.pfb')
82+
font = t1f.Type1Font(filename)
83+
assert font.prop['Weight'] == 'Bold'
84+
assert font.prop['isFixedPitch']
85+
assert font.prop['Encoding'][65] == 'A' # the font uses StandardEncoding
86+
(pos0, pos1), = font._pos['Encoding']
87+
assert font.parts[0][pos0:pos1] == b'/Encoding StandardEncoding'
88+
89+
90+
def test_tokenize():
91+
data = (b'1234/abc false -9.81 Foo <<[0 1 2]<0 1ef a\t>>>\n'
92+
b'(string with(nested\t\\) par)ens\\\\)')
93+
# 1 2 x 2 xx1
94+
# 1 and 2 are matching parens, x means escaped character
95+
n, w, num, kw, d = 'name', 'whitespace', 'number', 'keyword', 'delimiter'
96+
b, s = 'boolean', 'string'
97+
correct = [
98+
(num, 1234), (n, 'abc'), (w, ' '), (b, False), (w, ' '), (num, -9.81),
99+
(w, ' '), (kw, 'Foo'), (w, ' '), (d, '<<'), (d, '['), (num, 0),
100+
(w, ' '), (num, 1), (w, ' '), (num, 2), (d, ']'), (s, b'\x01\xef\xa0'),
101+
(d, '>>'), (w, '\n'), (s, 'string with(nested\t) par)ens\\')
102+
]
103+
correct_no_ws = [x for x in correct if x[0] != w]
104+
105+
def convert(tokens):
106+
return [(t.kind, t.value()) for t in tokens]
107+
108+
assert convert(t1f._tokenize(data, False)) == correct
109+
assert convert(t1f._tokenize(data, True)) == correct_no_ws
110+
111+
def bin_after(n):
112+
tokens = t1f._tokenize(data, True)
113+
result = []
114+
for _ in range(n):
115+
result.append(next(tokens))
116+
result.append(tokens.send(10))
117+
return convert(result)
118+
119+
for n in range(1, len(correct_no_ws)):
120+
result = bin_after(n)
121+
assert result[:-1] == correct_no_ws[:n]
122+
assert result[-1][0] == 'binary'
123+
assert isinstance(result[-1][1], bytes)
124+
125+
126+
def test_tokenize_errors():
127+
with pytest.raises(ValueError):
128+
list(t1f._tokenize(b'1234 (this (string) is unterminated\\)', True))
129+
with pytest.raises(ValueError):
130+
list(t1f._tokenize(b'/Foo<01234', True))
131+
with pytest.raises(ValueError):
132+
list(t1f._tokenize(b'/Foo<01234abcg>/Bar', True))
133+
134+
56135
def test_overprecision():
57136
# We used to output too many digits in FontMatrix entries and
58137
# ItalicAngle, which could make Type-1 parsers unhappy.

0 commit comments

Comments
 (0)