Added the ability to recreate the PS stream (#2504)

* added the ability to recreate the PS stream

This fixes #2503
This commit is contained in:
derwind 2022-02-10 23:53:12 +09:00 committed by GitHub
parent 8a139f921c
commit b437417b71
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 379 additions and 6 deletions

View File

@ -15,14 +15,17 @@ write(path, data, kind='OTHER', dohex=False)
part should be written as hexadecimal or binary, but only if kind part should be written as hexadecimal or binary, but only if kind
is 'OTHER'. is 'OTHER'.
""" """
import fontTools
from fontTools.misc import eexec from fontTools.misc import eexec
from fontTools.misc.macCreatorType import getMacCreatorAndType from fontTools.misc.macCreatorType import getMacCreatorAndType
from fontTools.misc.textTools import bytechr, byteord, bytesjoin from fontTools.misc.textTools import bytechr, byteord, bytesjoin, tobytes
from fontTools.misc.psOperators import _type1_pre_eexec_order, _type1_fontinfo_order, _type1_post_eexec_order
from fontTools.encodings.StandardEncoding import StandardEncoding
import os import os
import re import re
__author__ = "jvr" __author__ = "jvr"
__version__ = "1.0b2" __version__ = "1.0b3"
DEBUG = 0 DEBUG = 0
@ -65,8 +68,8 @@ class T1Font(object):
write(path, self.getData(), type, dohex) write(path, self.getData(), type, dohex)
def getData(self): def getData(self):
# XXX Todo: if the data has been converted to Python object, if not hasattr(self, "data"):
# recreate the PS stream self.data = self.createData()
return self.data return self.data
def getGlyphSet(self): def getGlyphSet(self):
@ -102,6 +105,148 @@ class T1Font(object):
subrs[i] = psCharStrings.T1CharString(charString[lenIV:], subrs=subrs) subrs[i] = psCharStrings.T1CharString(charString[lenIV:], subrs=subrs)
del self.data del self.data
def createData(self):
sf = self.font
eexec_began = False
eexec_dict = {}
lines = []
lines.extend([self._tobytes(f"%!FontType1-1.1: {sf['FontName']}"),
self._tobytes(f"%t1Font: ({fontTools.version})"),
self._tobytes(f"%%BeginResource: font {sf['FontName']}")])
# follow t1write.c:writeRegNameKeyedFont
size = 3 # Headroom for new key addition
size += 1 # FontMatrix is always counted
size += 1 + 1 # Private, CharStings
for key in font_dictionary_keys:
size += int(key in sf)
lines.append(self._tobytes(f"{size} dict dup begin"))
for key, value in sf.items():
if eexec_began:
eexec_dict[key] = value
continue
if key == "FontInfo":
fi = sf["FontInfo"]
# follow t1write.c:writeFontInfoDict
size = 3 # Headroom for new key addition
for subkey in FontInfo_dictionary_keys:
size += int(subkey in fi)
lines.append(self._tobytes(f"/FontInfo {size} dict dup begin"))
for subkey, subvalue in fi.items():
lines.extend(self._make_lines(subkey, subvalue))
lines.append(b"end def")
elif key in _type1_post_eexec_order: # usually 'Private'
eexec_dict[key] = value
eexec_began = True
else:
lines.extend(self._make_lines(key, value))
lines.append(b"end")
eexec_portion = self.encode_eexec(eexec_dict)
lines.append(bytesjoin([b"currentfile eexec ", eexec_portion]))
for _ in range(8):
lines.append(self._tobytes("0"*64))
lines.extend([b"cleartomark",
b"%%EndResource",
b"%%EOF"])
data = bytesjoin(lines, "\n")
return data
def encode_eexec(self, eexec_dict):
lines = []
# '-|', '|-', '|'
RD_key, ND_key, NP_key = None, None, None
for key, value in eexec_dict.items():
if key == "Private":
pr = eexec_dict["Private"]
# follow t1write.c:writePrivateDict
size = 3 # for RD, ND, NP
for subkey in Private_dictionary_keys:
size += int(subkey in pr)
lines.append(b"dup /Private")
lines.append(self._tobytes(f"{size} dict dup begin"))
for subkey, subvalue in pr.items():
if not RD_key and subvalue == RD_value:
RD_key = subkey
elif not ND_key and subvalue == ND_value:
ND_key = subkey
elif not NP_key and subvalue == PD_value:
NP_key = subkey
if subkey == 'OtherSubrs':
# XXX: assert that no flex hint is used
lines.append(self._tobytes(hintothers))
elif subkey == "Subrs":
# XXX: standard Subrs only
lines.append(b"/Subrs 5 array")
for i, subr_bin in enumerate(std_subrs):
encrypted_subr, R = eexec.encrypt(bytesjoin([char_IV, subr_bin]), 4330)
lines.append(bytesjoin([self._tobytes(f"dup {i} {len(encrypted_subr)} {RD_key} "), encrypted_subr, self._tobytes(f" {NP_key}")]))
lines.append(b'def')
lines.append(b"put")
else:
lines.extend(self._make_lines(subkey, subvalue))
elif key == "CharStrings":
lines.append(b"dup /CharStrings")
lines.append(self._tobytes(f"{len(eexec_dict['CharStrings'])} dict dup begin"))
for glyph_name, char_bin in eexec_dict["CharStrings"].items():
char_bin.compile()
encrypted_char, R = eexec.encrypt(bytesjoin([char_IV, char_bin.bytecode]), 4330)
lines.append(bytesjoin([self._tobytes(f"/{glyph_name} {len(encrypted_char)} {RD_key} "), encrypted_char, self._tobytes(f" {ND_key}")]))
lines.append(b"end put")
else:
lines.extend(self._make_lines(key, value))
lines.extend([b"end",
b"dup /FontName get exch definefont pop",
b"mark",
b"currentfile closefile\n"])
eexec_portion = bytesjoin(lines, "\n")
encrypted_eexec, R = eexec.encrypt(bytesjoin([eexec_IV, eexec_portion]), 55665)
return encrypted_eexec
def _make_lines(self, key, value):
if key == "FontName":
return [self._tobytes(f"/{key} /{value} def")]
if key in ["isFixedPitch", "ForceBold", "RndStemUp"]:
return [self._tobytes(f"/{key} {'true' if value else 'false'} def")]
elif key == "Encoding":
if value == StandardEncoding:
return [self._tobytes(f"/{key} StandardEncoding def")]
else:
# follow fontTools.misc.psOperators._type1_Encoding_repr
lines = []
lines.append(b"/Encoding 256 array")
lines.append(b"0 1 255 {1 index exch /.notdef put} for")
for i in range(256):
name = value[i]
if name != ".notdef":
lines.append(self._tobytes(f"dup {i} /{name} put"))
lines.append(b"def")
return lines
if isinstance(value, str):
return [self._tobytes(f"/{key} ({value}) def")]
elif isinstance(value, bool):
return [self._tobytes(f"/{key} {'true' if value else 'false'} def")]
elif isinstance(value, list):
return [self._tobytes(f"/{key} [{' '.join(str(v) for v in value)}] def")]
elif isinstance(value, tuple):
return [self._tobytes(f"/{key} {{{' '.join(str(v) for v in value)}}} def")]
else:
return [self._tobytes(f"/{key} {value} def")]
def _tobytes(self, s, errors="strict"):
return tobytes(s, self.encoding, errors)
# low level T1 data read and write functions # low level T1 data read and write functions
@ -367,3 +512,69 @@ def stringToLong(s):
for i in range(4): for i in range(4):
l += byteord(s[i]) << (i * 8) l += byteord(s[i]) << (i * 8)
return l return l
# PS stream helpers
font_dictionary_keys = list(_type1_pre_eexec_order)
# t1write.c:writeRegNameKeyedFont
# always counts following keys
font_dictionary_keys.remove("FontMatrix")
FontInfo_dictionary_keys = list(_type1_fontinfo_order)
# extend because AFDKO tx may use following keys
FontInfo_dictionary_keys.extend([
"FSType",
"Copyright",
])
Private_dictionary_keys = [
# We don't know what names will be actually used.
# "RD",
# "ND",
# "NP",
"Subrs",
"OtherSubrs",
"UniqueID",
"BlueValues",
"OtherBlues",
"FamilyBlues",
"FamilyOtherBlues",
"BlueScale",
"BlueShift",
"BlueFuzz",
"StdHW",
"StdVW",
"StemSnapH",
"StemSnapV",
"ForceBold",
"LanguageGroup",
"password",
"lenIV",
"MinFeature",
"RndStemUp",
]
# t1write_hintothers.h
hintothers = """/OtherSubrs[{}{}{}{systemdict/internaldict known not{pop 3}{1183615869
systemdict/internaldict get exec dup/startlock known{/startlock get exec}{dup
/strtlck known{/strtlck get exec}{pop 3}ifelse}ifelse}ifelse}executeonly]def"""
# t1write.c:saveStdSubrs
std_subrs = [
# 3 0 callother pop pop setcurrentpoint return
b"\x8e\x8b\x0c\x10\x0c\x11\x0c\x11\x0c\x21\x0b",
# 0 1 callother return
b"\x8b\x8c\x0c\x10\x0b",
# 0 2 callother return
b"\x8b\x8d\x0c\x10\x0b",
# return
b"\x0b",
# 3 1 3 callother pop callsubr return
b"\x8e\x8c\x8e\x0c\x10\x0c\x11\x0a\x0b"
]
# follow t1write.c:writeRegNameKeyedFont
eexec_IV = b"cccc"
char_IV = b"\x0c\x0c\x0c\x0c"
RD_value = ("string", "currentfile", "exch", "readstring", "pop")
ND_value = ("def",)
PD_value = ("put",)

View File

@ -0,0 +1,73 @@
%!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 a0b00ed5187d9c0f1ecdf51878c3aa5c80164785b2862e5e248314a0dd481e1d
8115d1b5316c01657582d7e6cf3fa3e1aa00733eacc08e989313fc4f69225c12
e1cce25e5f99b47841c1a01a3778ce0d2cd604a925e1a95952caddc379227509
e885feae9bdf476bbc2a66a211f868f2cfc17055dcf167e621fd9f41a20f2647
a3433004f074ba98472481205cebb34224112701c802754418c103b503fdcc51
ff373f66fea5ab6b7578cc881c6c46de4327adf847412c9df1700b8402f28395
fc36b2e8826ef268b1f4c0f79d3612e999d7a22412f8fcd4398cba1b4db9b0a1
a050a7777e3b04617c75f9373f76b7a1daffd6423d0bd68c190f563fa54ff699
bbe5e5ff80d2329a8f7c5f85d10fe3410cf665fb3e3271d7d92bdbeac0bd6a11
a3726b6bbc45a143b60b22ae9f5cb25b595bfd3797d0aaca765516fa103b5d01
e016ca279efa8c90b062ee151238c62dda8d109bea1814ab49ab93034196e785
a7629fc297ddb0b5ba7bb1da4d9b69c35c4038082747cf0180fb387f1925640a
239c91872b562233065484922f59063bd42241053bf69317d22dbcbfc869996a
11f5f628930da9a2d48d327804add904de6bb0bd84b6584eacdbc2760af6d44f
d4a765c1829b2b650773894ca9d7e303b8dfc43804e4b60a7eb1506139fa96b8
0f70238f69a4ce6ad2a7e20f344e9f69930fe1cc30688775b2875de4f79039e0
8c0dc7db7470d9773ad4345d50c1fbdb9089c91b6d170bc3e39b61b3d86e3a78
1e31d872384c85a6241dc349d0af38785e1f37426ac0ca1b7369bf00c947953a
191be897b89a84d903f590ee852b34433d892f9f60e6b2236e6dee1562d69c9d
e80205d7aa49f9c7d01f8afb16babf9979d3dccf28b23160934d425f503d82d1
d602bdbea6c0378fa6631f4dd3709dc6db7395663356df1eb3b6ee42d8006e0f
9c1d0402f1bc9cf656bc0f75f51db76781f447139a4babad33af6309d271b472
7607ca92ff947e561d94a66eda64788fd32c7427341bf5d365a0cb4bbf0a0534
1a92824f0a80a678ffd6320d9feb538cde66cb65a5d3a7a53d601732fa83efaf
05effa0938b101cfd2dac6a80631c10a7d0a2ea5950a2af3de5afbab29590986
27fc0526581792c06db8374568e14023384584bba23ac87921af46943a23e0ea
7aa4c0f6a57cbe7c91fac9d612e36f721f518945ce0f70cf3084cf1565480cd9
f75e6183eefb58d4936a4c8ff76b80b3b9b12d876b3f0c8773a105cf2a24d87e
99feed1ca9114d37698f07763c538a011672b1fc1aa9ed610134f7d7
0000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000
cleartomark
%%EndResource
%%EOF

View File

@ -3,6 +3,7 @@ import os
import sys import sys
from fontTools import t1Lib from fontTools import t1Lib
from fontTools.pens.basePen import NullPen from fontTools.pens.basePen import NullPen
from fontTools.misc.psCharStrings import T1CharString
import random import random
@ -13,6 +14,8 @@ LWFN = os.path.join(DATADIR, 'TestT1-Regular.lwfn')
PFA = os.path.join(DATADIR, 'TestT1-Regular.pfa') PFA = os.path.join(DATADIR, 'TestT1-Regular.pfa')
PFB = os.path.join(DATADIR, 'TestT1-Regular.pfb') PFB = os.path.join(DATADIR, 'TestT1-Regular.pfb')
WEIRD_ZEROS = os.path.join(DATADIR, 'TestT1-weird-zeros.pfa') WEIRD_ZEROS = os.path.join(DATADIR, 'TestT1-weird-zeros.pfa')
# ellipsis is hinted with 55 131 296 131 537 131 vstem3 0 122 hstem
ELLIPSIS_HINTED = os.path.join(DATADIR, 'TestT1-ellipsis-hinted.pfa')
class FindEncryptedChunksTest(unittest.TestCase): class FindEncryptedChunksTest(unittest.TestCase):
@ -52,22 +55,39 @@ class ReadWriteTest(unittest.TestCase):
data = self.write(font, 'PFB') data = self.write(font, 'PFB')
self.assertEqual(font.getData(), data) self.assertEqual(font.getData(), data)
def test_read_and_parse_pfa_write_pfb(self):
font = t1Lib.T1Font(PFA)
font.parse()
saved_font = self.write(font, 'PFB', dohex=False, doparse=True)
self.assertTrue(same_dicts(font.font, saved_font))
def test_read_pfb_write_pfa(self): def test_read_pfb_write_pfa(self):
font = t1Lib.T1Font(PFB) font = t1Lib.T1Font(PFB)
# 'OTHER' == 'PFA' # 'OTHER' == 'PFA'
data = self.write(font, 'OTHER', dohex=True) data = self.write(font, 'OTHER', dohex=True)
self.assertEqual(font.getData(), data) self.assertEqual(font.getData(), data)
def test_read_and_parse_pfb_write_pfa(self):
font = t1Lib.T1Font(PFB)
font.parse()
# 'OTHER' == 'PFA'
saved_font = self.write(font, 'OTHER', dohex=True, doparse=True)
self.assertTrue(same_dicts(font.font, saved_font))
def test_read_with_path(self): def test_read_with_path(self):
import pathlib import pathlib
font = t1Lib.T1Font(pathlib.Path(PFB)) font = t1Lib.T1Font(pathlib.Path(PFB))
@staticmethod @staticmethod
def write(font, outtype, dohex=False): def write(font, outtype, dohex=False, doparse=False):
temp = os.path.join(DATADIR, 'temp.' + outtype.lower()) temp = os.path.join(DATADIR, 'temp.' + outtype.lower())
try: try:
font.saveAs(temp, outtype, dohex=dohex) font.saveAs(temp, outtype, dohex=dohex)
newfont = t1Lib.T1Font(temp) newfont = t1Lib.T1Font(temp)
if doparse:
newfont.parse()
data = newfont.font
else:
data = newfont.getData() data = newfont.getData()
finally: finally:
if os.path.exists(temp): if os.path.exists(temp):
@ -107,6 +127,75 @@ class T1FontTest(unittest.TestCase):
self.assertTrue(hasattr(aglyph, 'width')) self.assertTrue(hasattr(aglyph, 'width'))
class EditTest(unittest.TestCase):
def test_edit_pfa(self):
font = t1Lib.T1Font(PFA)
ellipsis = font.getGlyphSet()["ellipsis"]
ellipsis.decompile()
program = []
for v in ellipsis.program:
try:
program.append(int(v))
except:
program.append(v)
if v == 'hsbw':
hints = [55, 131, 296, 131, 537, 131, 'vstem3', 0, 122, 'hstem']
program.extend(hints)
ellipsis.program = program
# 'OTHER' == 'PFA'
saved_font = self.write(font, 'OTHER', dohex=True, doparse=True)
hinted_font = t1Lib.T1Font(ELLIPSIS_HINTED)
hinted_font.parse()
self.assertTrue(same_dicts(hinted_font.font, saved_font))
@staticmethod
def write(font, outtype, dohex=False, doparse=False):
temp = os.path.join(DATADIR, 'temp.' + outtype.lower())
try:
font.saveAs(temp, outtype, dohex=dohex)
newfont = t1Lib.T1Font(temp)
if doparse:
newfont.parse()
data = newfont.font
else:
data = newfont.getData()
finally:
if os.path.exists(temp):
os.remove(temp)
return data
def same_dicts(dict1, dict2):
if dict1.keys() != dict2.keys():
return False
for key, value in dict1.items():
if isinstance(value, dict):
if not same_dicts(value, dict2[key]):
return False
elif isinstance(value, list):
if len(value) != len(dict2[key]):
return False
for elem1, elem2 in zip(value, dict2[key]):
if isinstance(elem1, T1CharString):
elem1.compile()
elem2.compile()
if elem1.bytecode != elem2.bytecode:
return False
else:
if elem1 != elem2:
return False
elif isinstance(value, T1CharString):
value.compile()
dict2[key].compile()
if value.bytecode != dict2[key].bytecode:
return False
else:
if value != dict2[key]:
return False
return True
if __name__ == '__main__': if __name__ == '__main__':
import sys import sys
sys.exit(unittest.main()) sys.exit(unittest.main())