Repository: fonttools/fonttools

Issue:
fontTools.t1Lib.T1Error: can't find end of eexec part
The T1 font parser is unable to handle some fonts (which conform to the spec). It complains:

```python
>>> f = T1Font('some-t1-font')
fontTools.t1Lib.T1Error: can't find end of eexec part
```
In particular, this happens with (spec-conformant) fonts that don't have 64 consecutive ASCII `0`s immediately after the `eexec` section:

```bash
$ tail -c 550 some-t1-font | xxd
00000000: 0a86 c365 74ba 6d9f 36e9 3030 3030 3030  ...et.m.6.000000
00000010: 3030 3030 3030 3030 3030 3030 3030 3030  0000000000000000
00000020: 3030 3030 3030 3030 3030 0a30 3030 3030  0000000000.00000
00000030: 3030 3030 3030 3030 3030 3030 3030 3030  0000000000000000
00000040: 3030 3030 3030 3030 3030 300a 3030 3030  00000000000.0000
00000050: 3030 3030 3030 3030 3030 3030 3030 3030  0000000000000000
00000060: 3030 3030 3030 3030 3030 3030 0a30 3030  000000000000.000
00000070: 3030 3030 3030 3030 3030 3030 3030 3030  0000000000000000
00000080: 3030 3030 3030 3030 3030 3030 300a 3030  0000000000000.00
00000090: 3030 3030 3030 3030 3030 3030 3030 3030  0000000000000000
000000a0: 3030 3030 3030 3030 3030 3030 3030 0a30  00000000000000.0
000000b0: 3030 3030 3030 3030 3030 3030 3030 3030  0000000000000000
000000c0: 3030 3030 3030 3030 3030 3030 3030 300a  000000000000000.
000000d0: 3030 3030 3030 3030 3030 3030 3030 3030  0000000000000000
000000e0: 3030 3030 3030 3030 3030 3030 3030 3030  0000000000000000
000000f0: 0a30 3030 3030 3030 3030 3030 3030 3030  .000000000000000
00000100: 3030 3030 3030 3030 3030 3030 3030 3030  0000000000000000
00000110: 300a 3030 3030 3030 3030 3030 3030 3030  0.00000000000000
00000120: 3030 3030 3030 3030 3030 3030 3030 3030  0000000000000000
00000130: 3030 0a30 3030 3030 3030 3030 3030 3030  00.0000000000000
00000140: 3030 3030 3030 3030 3030 3030 3030 3030  0000000000000000
00000150: 3030 300a 3030 3030 3030 3030 3030 3030  000.000000000000
00000160: 3030 3030 3030 3030 3030 3030 3030 3030  0000000000000000
00000170: 3030 3030 0a30 3030 3030 3030 3030 3030  0000.00000000000
00000180: 3030 3030 3030 3030 3030 3030 3030 3030  0000000000000000
00000190: 3030 3030 300a 3030 3030 3030 3030 3030  00000.0000000000
000001a0: 3030 3030 3030 3030 3030 3030 3030 3030  0000000000000000
000001b0: 3030 3030 3030 0a30 3030 3030 3030 3030  000000.000000000
000001c0: 3030 3030 3030 3030 3030 3030 3030 3030  0000000000000000
000001d0: 3030 3030 3030 300a 3030 3030 3030 3030  0000000.00000000
000001e0: 3030 3030 3030 3030 3030 3030 3030 3030  0000000000000000
000001f0: 3030 3030 3030 3030 0a30 3030 3030 3030  00000000.0000000
00000200: 3030 3030 3030 3030 3030 3030 3030 3030  0000000000000000
00000210: 3030 3030 3030 3030 300a 636c 6561 7274  000000000.cleart
00000220: 6f6d 6172 6b0a                           omark.
```

The problem is that this file has runs of 32 ASCII `0`'s separated by line feeds. According to [the spec](https://www-cdf.fnal.gov/offline/PostScript/T1_SPEC.PDF) (page 14) this is valid:

> The text encrypted by **eexec** must be followed by 512 ASCII zeros. There may be white space characters (blank, tab, carriage return or line feed) interspersed among these zeros.

The culprit here is:

https://github.com/fonttools/fonttools/blob/f9d12be58fde76c5f8bbd8ac207223874494c713/Lib/fontTools/t1Lib/__init__.py#L276

https://github.com/fonttools/fonttools/blob/f9d12be58fde76c5f8bbd8ac207223874494c713/Lib/fontTools/t1Lib/__init__.py#L308-L329

A contiguous run of 64 ASCII `0`s is expected. But it would be to spec to have `0\n0\t0\r0 0` etc. trailing the eexec section.

I have several different T1 fonts that exhibit this behavior (one such font is New Century Schoolbook--although I don't want to get you DMCA'd by uploading it). They can all be read with macOS Font Book and FontForge. And indeed, if I change `EEXECEND = b'0' * 64` to `EEXECEND = b'0' * 32`, fontTools successfully parses the font and is capable of drawing the glyphs.


Discussion:

Resolving patch:
diff --git a/Lib/fontTools/t1Lib/__init__.py b/Lib/fontTools/t1Lib/__init__.py
--- a/Lib/fontTools/t1Lib/__init__.py
+++ b/Lib/fontTools/t1Lib/__init__.py
@@ -273,7 +273,9 @@ def writeOther(path, data, dohex=False):
 # decryption tools
 
 EEXECBEGIN = b"currentfile eexec"
-EEXECEND = b'0' * 64
+# The spec allows for 512 ASCII zeros interrupted by arbitrary whitespace to
+# follow eexec
+EEXECEND = re.compile(b'(0[ \t\r\n]*){512}', flags=re.M)
 EEXECINTERNALEND = b"currentfile closefile"
 EEXECBEGINMARKER = b"%-- eexec start\r"
 EEXECENDMARKER = b"%-- eexec end\r"
@@ -312,9 +314,10 @@ def findEncryptedChunks(data):
 		if eBegin < 0:
 			break
 		eBegin = eBegin + len(EEXECBEGIN) + 1
-		eEnd = data.find(EEXECEND, eBegin)
-		if eEnd < 0:
+		endMatch = EEXECEND.search(data, eBegin)
+		if endMatch is None:
 			raise T1Error("can't find end of eexec part")
+		eEnd = endMatch.start()
 		cypherText = data[eBegin:eEnd + 2]
 		if isHex(cypherText[:4]):
 			cypherText = deHexString(cypherText)


Test patch:
diff --git a/Tests/t1Lib/data/TestT1-weird-zeros.pfa b/Tests/t1Lib/data/TestT1-weird-zeros.pfa
new file mode 100644
--- /dev/null
+++ b/Tests/t1Lib/data/TestT1-weird-zeros.pfa
@@ -0,0 +1,63 @@
+%!FontType1-1.1: TestT1-Regular 1.0
+%%BeginResource: font TestT1-Regular
+12 dict dup begin
+/FontType 1 def
+/FontName /TestT1-Regular def
+/FontInfo 14 dict dup begin
+/version (1.0) def
+/Notice (Test T1 is not a trademark of FontTools.) def
+/Copyright (Copyright c 2015 by FontTools. No rights reserved.) def
+/FullName (Test T1) def
+/FamilyName (Test T1) def
+/Weight (Regular) def
+/ItalicAngle 0.000000 def
+/isFixedPitch false def
+/UnderlinePosition -75.000000 def
+/UnderlineThickness 50.000000 def
+/FSType 0 def
+end def
+/PaintType 0 def
+/FontMatrix [0.001 0 0 0.001 0 0] def
+/Encoding 256 array
+0 1 255 {1 index exch /.notdef put} for
+def
+/FontBBox {50.000000 0.000000 668.000000 750.000000} def
+end
+currentfile eexec bab431ea06bb0a1031e1aa11919e714ac1ac5197cb08b39a4d7e746fca0af12d89ac0ebd1bc11ab1
+b3887b922efcec739534242d2fd22e7c30e3edce24b93798627e1ac3387816a8c4b84d76047dada8
+28b2ad27c5603046fecbc2a97adc5c37a68912324d2d435f2ee0ccc38df10ba1271a1c9af8897a6d
+6e425cd7d18fd6bd64c2adadb74365bc101a850841669886291e158cbfa7f204b3fe0ba49ffe0c80
+4f6795d32eb770c5fcd38a3879c06a4bb87b2d3ab100d8c2b5f89e9be99248575575025c66381446
+e4d9183674880aef57fb2032a1e00431133b16f6d758de7c3d0c48a0fada1d40034742a69fb3a6f9
+450d2251e659158a04697cbfa70907346d27d37ef683284385c44a1b5089bd29b4629b6483122dc8
+cbce7327bdc33dd30e6fcdb346c0ddaf433a5ac740423aa35639b2386673832f5ae8cc380e9703ba
+d3369533bfa85af9f56a090c9d97f5fc26ed102c07b647137e83632be51a65a532bd26430b59a31c
+3cb037ded351c1d4e944733feb30a3e6f81c1a7b74ac4e0eadbe705412d47991c246e8820876bbc6
+1f6a3e264ae6b2ad4b864b0d7abee289308bea26eb15d00d2b9103861386e0a5f1802ba06f916810
+62110d2b1c3641806f78eea365614f440b580185e84bac6f87bee36108d95174c786600cf0e9dc4a
+5545d1a84cfe8392115c0b7027c17fd460481d21f684af32204085690946327bfded992852645149
+8d44150d2495bd2efe0db6a450c6e28d0a52ca234e252129d5095596b0d8de096682d2eb00bc8320
+f257fd653b05a22eab7a193ccc315a6ee274a03ff1fdf443b310157a02656ca4b06c581dca8ced72
+c6ddcab26eb856ad1093452c587438b7f8408c1311e19254955914612c09828fd4d4fc2b8b0406ea
+2ee38348a8bdab88a77b8033366b2e469834c01b7bd73207b7c67756937c7a9232947fde2e0ea327
+7b7d610e601b91389ccbcdd813c87db5333c0c723e48d3ef69285f246327978ce68ae9081076a227
+1a962a2a10e2b1147ec40b0f6553a00c8b329118569d16fe04a4fa195caf1b04c52c9a562b72e0cd
+e411d747af796b9d2fb086ed927efb0e5fc9f50aa18aaf4949cba0de0805210620a19eec4319dfef
+a74d9d13d16f8ad793323a231347e6b40022a1100c1e064b8679c1da63a26dfb217a6037096ad796
+320da5a9d0526eed51d7d64d3223e285c1a8c70780c59ecc9dd9bc90a0f84ffa038834918cebe247
+f6e8fa4ca0654019196388f2df008e63bc32c8e5e686dbb69193b7749638c22b389fb1f090fbb007
+fdb8a6ee4e4b29e123fe1652fe72239bd2c8
+00000000000 00000000000000000000000000000000000000000000000000000
+0000000000000000000000000000000	000000000000000000000000000000000
+00000000000000000000000
+00000000000000000000000000000000000000000
+0000000000000000000000000000000000000000 000000000000000000000000
+00
+00000000000000000000000000000000000000000000000000000000000000
+00000000000000000000000000000000000000000000000000000	00000000000
+00000000000000000000 00000000000000000000000000000000000000000000
+00000000000000000000
+00000000000000000000000000000000000000000000
+cleartomark
+%%EndResource
+%%EOF
diff --git a/Tests/t1Lib/t1Lib_test.py b/Tests/t1Lib/t1Lib_test.py
--- a/Tests/t1Lib/t1Lib_test.py
+++ b/Tests/t1Lib/t1Lib_test.py
@@ -14,6 +14,7 @@
 LWFN = os.path.join(DATADIR, 'TestT1-Regular.lwfn')
 PFA = os.path.join(DATADIR, 'TestT1-Regular.pfa')
 PFB = os.path.join(DATADIR, 'TestT1-Regular.pfb')
+WEIRD_ZEROS = os.path.join(DATADIR, 'TestT1-weird-zeros.pfa')
 
 
 class FindEncryptedChunksTest(unittest.TestCase):
@@ -28,6 +29,14 @@ def test_findEncryptedChunks(self):
 		self.assertTrue(chunks[1][0])
 		self.assertFalse(chunks[2][0])
 
+	def test_findEncryptedChunks_weird_zeros(self):
+		with open(WEIRD_ZEROS, 'rb') as f:
+			data = f.read()
+
+		# Just assert that this doesn't raise any exception for not finding the
+		# end of eexec
+		t1Lib.findEncryptedChunks(data)
+
 
 class DecryptType1Test(unittest.TestCase):