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:
parent
8a139f921c
commit
b437417b71
@ -15,14 +15,17 @@ write(path, data, kind='OTHER', dohex=False)
|
||||
part should be written as hexadecimal or binary, but only if kind
|
||||
is 'OTHER'.
|
||||
"""
|
||||
import fontTools
|
||||
from fontTools.misc import eexec
|
||||
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 re
|
||||
|
||||
__author__ = "jvr"
|
||||
__version__ = "1.0b2"
|
||||
__version__ = "1.0b3"
|
||||
DEBUG = 0
|
||||
|
||||
|
||||
@ -65,8 +68,8 @@ class T1Font(object):
|
||||
write(path, self.getData(), type, dohex)
|
||||
|
||||
def getData(self):
|
||||
# XXX Todo: if the data has been converted to Python object,
|
||||
# recreate the PS stream
|
||||
if not hasattr(self, "data"):
|
||||
self.data = self.createData()
|
||||
return self.data
|
||||
|
||||
def getGlyphSet(self):
|
||||
@ -102,6 +105,148 @@ class T1Font(object):
|
||||
subrs[i] = psCharStrings.T1CharString(charString[lenIV:], subrs=subrs)
|
||||
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
|
||||
|
||||
@ -367,3 +512,69 @@ def stringToLong(s):
|
||||
for i in range(4):
|
||||
l += byteord(s[i]) << (i * 8)
|
||||
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",)
|
||||
|
73
Tests/t1Lib/data/TestT1-ellipsis-hinted.pfa
Normal file
73
Tests/t1Lib/data/TestT1-ellipsis-hinted.pfa
Normal 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
|
@ -3,6 +3,7 @@ import os
|
||||
import sys
|
||||
from fontTools import t1Lib
|
||||
from fontTools.pens.basePen import NullPen
|
||||
from fontTools.misc.psCharStrings import T1CharString
|
||||
import random
|
||||
|
||||
|
||||
@ -13,6 +14,8 @@ 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')
|
||||
# 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):
|
||||
@ -52,23 +55,40 @@ class ReadWriteTest(unittest.TestCase):
|
||||
data = self.write(font, 'PFB')
|
||||
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):
|
||||
font = t1Lib.T1Font(PFB)
|
||||
# 'OTHER' == 'PFA'
|
||||
data = self.write(font, 'OTHER', dohex=True)
|
||||
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):
|
||||
import pathlib
|
||||
font = t1Lib.T1Font(pathlib.Path(PFB))
|
||||
|
||||
@staticmethod
|
||||
def write(font, outtype, dohex=False):
|
||||
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)
|
||||
data = newfont.getData()
|
||||
if doparse:
|
||||
newfont.parse()
|
||||
data = newfont.font
|
||||
else:
|
||||
data = newfont.getData()
|
||||
finally:
|
||||
if os.path.exists(temp):
|
||||
os.remove(temp)
|
||||
@ -107,6 +127,75 @@ class T1FontTest(unittest.TestCase):
|
||||
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__':
|
||||
import sys
|
||||
sys.exit(unittest.main())
|
||||
|
Loading…
x
Reference in New Issue
Block a user