From b437417b7194afbf7a22bf25b43e32326af68131 Mon Sep 17 00:00:00 2001 From: derwind Date: Thu, 10 Feb 2022 23:53:12 +0900 Subject: [PATCH] Added the ability to recreate the PS stream (#2504) * added the ability to recreate the PS stream This fixes #2503 --- Lib/fontTools/t1Lib/__init__.py | 219 +++++++++++++++++++- Tests/t1Lib/data/TestT1-ellipsis-hinted.pfa | 73 +++++++ Tests/t1Lib/t1Lib_test.py | 93 ++++++++- 3 files changed, 379 insertions(+), 6 deletions(-) create mode 100644 Tests/t1Lib/data/TestT1-ellipsis-hinted.pfa diff --git a/Lib/fontTools/t1Lib/__init__.py b/Lib/fontTools/t1Lib/__init__.py index d07d78c05..a74f9a474 100644 --- a/Lib/fontTools/t1Lib/__init__.py +++ b/Lib/fontTools/t1Lib/__init__.py @@ -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",) diff --git a/Tests/t1Lib/data/TestT1-ellipsis-hinted.pfa b/Tests/t1Lib/data/TestT1-ellipsis-hinted.pfa new file mode 100644 index 000000000..3fc6696d2 --- /dev/null +++ b/Tests/t1Lib/data/TestT1-ellipsis-hinted.pfa @@ -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 \ No newline at end of file diff --git a/Tests/t1Lib/t1Lib_test.py b/Tests/t1Lib/t1Lib_test.py index 947582327..3e639a582 100644 --- a/Tests/t1Lib/t1Lib_test.py +++ b/Tests/t1Lib/t1Lib_test.py @@ -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())