From 17864f78ce9269a9679ea469c80a88a7c023b31c Mon Sep 17 00:00:00 2001 From: Shimon Doodkin Date: Sat, 19 Apr 2025 07:37:51 +0300 Subject: [PATCH 1/5] fix logic error in parseEncoding1 - range-based encoding if two ranges existed one after another it would fail. --- Lib/fontTools/cffLib/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/fontTools/cffLib/__init__.py b/Lib/fontTools/cffLib/__init__.py index d75e23b750..5278380e7a 100644 --- a/Lib/fontTools/cffLib/__init__.py +++ b/Lib/fontTools/cffLib/__init__.py @@ -1733,13 +1733,13 @@ def parseEncoding1(charset, file, haveSupplement, strings): nRanges = readCard8(file) encoding = [".notdef"] * 256 glyphID = 1 - for i in range(nRanges): + for _ in range(nRanges): code = readCard8(file) nLeft = readCard8(file) - for glyphID in range(glyphID, glyphID + nLeft + 1): + for _ in range(nLeft + 1): encoding[code] = charset[glyphID] - code = code + 1 - glyphID = glyphID + 1 + code += 1 + glyphID += 1 return encoding From f057585331e7ea1bbbae0d0f39d491b576fc3e37 Mon Sep 17 00:00:00 2001 From: Shimon Doodkin Date: Sat, 19 Apr 2025 07:38:57 +0300 Subject: [PATCH 2/5] fix read supplement encoding --- Lib/fontTools/cffLib/__init__.py | 76 ++++++++++++++++++++++++-------- 1 file changed, 58 insertions(+), 18 deletions(-) diff --git a/Lib/fontTools/cffLib/__init__.py b/Lib/fontTools/cffLib/__init__.py index 5278380e7a..2ce6d90be2 100644 --- a/Lib/fontTools/cffLib/__init__.py +++ b/Lib/fontTools/cffLib/__init__.py @@ -1663,25 +1663,27 @@ def _read(self, parent, value): return "StandardEncoding" elif value == 1: return "ExpertEncoding" + # custom encoding at offset `value` + assert value > 1 + file = parent.file + file.seek(value) + log.log(DEBUG, "loading Encoding at %s", value) + fmt = readCard8(file) + haveSupplement = bool(fmt & 0x80) + fmt = fmt & 0x7F + + if fmt == 0: + encoding = parseEncoding0( + parent.charset, file, haveSupplement, parent.strings + ) + elif fmt == 1: + encoding = parseEncoding1( + parent.charset, file, haveSupplement, parent.strings + ) else: - assert value > 1 - file = parent.file - file.seek(value) - log.log(DEBUG, "loading Encoding at %s", value) - fmt = readCard8(file) - haveSupplement = fmt & 0x80 - if haveSupplement: - raise NotImplementedError("Encoding supplements are not yet supported") - fmt = fmt & 0x7F - if fmt == 0: - encoding = parseEncoding0( - parent.charset, file, haveSupplement, parent.strings - ) - elif fmt == 1: - encoding = parseEncoding1( - parent.charset, file, haveSupplement, parent.strings - ) - return encoding + raise ValueError(f"Unknown Encoding format: {fmt}") + + return encoding def write(self, parent, value): if value == "StandardEncoding": @@ -1719,17 +1721,51 @@ def xmlRead(self, name, attrs, content, parent): return encoding +def readSID(file): + """Read a String ID (SID) — 2-byte unsigned integer.""" + data = file.read(2) + if len(data) != 2: + raise EOFError("Unexpected end of file while reading SID") + return struct.unpack(">H", data)[0] # big-endian uint16 + +def parseEncodingSupplement(file, encoding, strings): + """ + Parse the CFF Encoding supplement data: + - nSups: number of supplementary mappings + - each mapping: (code, SID) pair + and apply them to the `encoding` list in place. + """ + nSups = readCard8(file) + for _ in range(nSups): + code = readCard8(file) + sid = readSID(file) + name = strings[sid] + encoding[code] = name + + def parseEncoding0(charset, file, haveSupplement, strings): + """ + Format 0: simple list of codes. + After reading the base table, optionally parse the supplement. + """ nCodes = readCard8(file) encoding = [".notdef"] * 256 for glyphID in range(1, nCodes + 1): code = readCard8(file) if code != 0: encoding[code] = charset[glyphID] + + if haveSupplement: + parseEncodingSupplement(file, encoding, strings) + return encoding def parseEncoding1(charset, file, haveSupplement, strings): + """ + Format 1: range-based encoding. + After reading the base ranges, optionally parse the supplement. + """ nRanges = readCard8(file) encoding = [".notdef"] * 256 glyphID = 1 @@ -1740,6 +1776,10 @@ def parseEncoding1(charset, file, haveSupplement, strings): encoding[code] = charset[glyphID] code += 1 glyphID += 1 + + if haveSupplement: + parseEncodingSupplement(file, encoding, strings) + return encoding From b6ec2d57fc8763988e739f4e5b10026237947cdb Mon Sep 17 00:00:00 2001 From: Shimon Doodkin Date: Sat, 19 Apr 2025 09:04:28 +0300 Subject: [PATCH 3/5] fix read supplement encoding - refactor --- Lib/fontTools/cffLib/__init__.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/Lib/fontTools/cffLib/__init__.py b/Lib/fontTools/cffLib/__init__.py index 2ce6d90be2..052528c8f3 100644 --- a/Lib/fontTools/cffLib/__init__.py +++ b/Lib/fontTools/cffLib/__init__.py @@ -1674,15 +1674,18 @@ def _read(self, parent, value): if fmt == 0: encoding = parseEncoding0( - parent.charset, file, haveSupplement, parent.strings + parent.charset, file ) elif fmt == 1: encoding = parseEncoding1( - parent.charset, file, haveSupplement, parent.strings + parent.charset, file ) else: raise ValueError(f"Unknown Encoding format: {fmt}") + if haveSupplement: + parseEncodingSupplement(file, encoding, parent.strings) + return encoding def write(self, parent, value): @@ -1742,8 +1745,7 @@ def parseEncodingSupplement(file, encoding, strings): name = strings[sid] encoding[code] = name - -def parseEncoding0(charset, file, haveSupplement, strings): +def parseEncoding0(charset, file): """ Format 0: simple list of codes. After reading the base table, optionally parse the supplement. @@ -1755,13 +1757,12 @@ def parseEncoding0(charset, file, haveSupplement, strings): if code != 0: encoding[code] = charset[glyphID] - if haveSupplement: - parseEncodingSupplement(file, encoding, strings) + return encoding -def parseEncoding1(charset, file, haveSupplement, strings): +def parseEncoding1(charset, file): """ Format 1: range-based encoding. After reading the base ranges, optionally parse the supplement. @@ -1777,8 +1778,6 @@ def parseEncoding1(charset, file, haveSupplement, strings): code += 1 glyphID += 1 - if haveSupplement: - parseEncodingSupplement(file, encoding, strings) return encoding From 9a775b44ab0b2697988b90756edba8b2c5275993 Mon Sep 17 00:00:00 2001 From: Shimon Doodkin Date: Mon, 28 Apr 2025 01:05:13 +0300 Subject: [PATCH 4/5] lint --- Lib/fontTools/cffLib/__init__.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/Lib/fontTools/cffLib/__init__.py b/Lib/fontTools/cffLib/__init__.py index 052528c8f3..f168101ea1 100644 --- a/Lib/fontTools/cffLib/__init__.py +++ b/Lib/fontTools/cffLib/__init__.py @@ -1673,19 +1673,15 @@ def _read(self, parent, value): fmt = fmt & 0x7F if fmt == 0: - encoding = parseEncoding0( - parent.charset, file - ) + encoding = parseEncoding0(parent.charset, file) elif fmt == 1: - encoding = parseEncoding1( - parent.charset, file - ) + encoding = parseEncoding1(parent.charset, file) else: raise ValueError(f"Unknown Encoding format: {fmt}") if haveSupplement: parseEncodingSupplement(file, encoding, parent.strings) - + return encoding def write(self, parent, value): @@ -1731,6 +1727,7 @@ def readSID(file): raise EOFError("Unexpected end of file while reading SID") return struct.unpack(">H", data)[0] # big-endian uint16 + def parseEncodingSupplement(file, encoding, strings): """ Parse the CFF Encoding supplement data: @@ -1745,6 +1742,7 @@ def parseEncodingSupplement(file, encoding, strings): name = strings[sid] encoding[code] = name + def parseEncoding0(charset, file): """ Format 0: simple list of codes. @@ -1757,8 +1755,6 @@ def parseEncoding0(charset, file): if code != 0: encoding[code] = charset[glyphID] - - return encoding @@ -1778,7 +1774,6 @@ def parseEncoding1(charset, file): code += 1 glyphID += 1 - return encoding From 789952818d41feecce587fde212b291436fd812c Mon Sep 17 00:00:00 2001 From: Shimon Doodkin Date: Mon, 28 Apr 2025 01:55:59 +0300 Subject: [PATCH 5/5] add test --- Tests/cffLib/cffLib_test.py | 23 +++++++++++++++---- Tests/cffLib/data/TestSupplementEncoding.cff | Bin 0 -> 1930 bytes 2 files changed, 19 insertions(+), 4 deletions(-) create mode 100644 Tests/cffLib/data/TestSupplementEncoding.cff diff --git a/Tests/cffLib/cffLib_test.py b/Tests/cffLib/cffLib_test.py index 7146e5d660..8f30fbf26d 100644 --- a/Tests/cffLib/cffLib_test.py +++ b/Tests/cffLib/cffLib_test.py @@ -1,9 +1,15 @@ -from fontTools.cffLib import TopDict, PrivateDict, CharStrings +import os +import sys + +libdir = os.path.join( + os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))), "Lib" +) +sys.path.insert(0, libdir) + +from fontTools.cffLib import TopDict, PrivateDict, CharStrings, CFFFontSet from fontTools.misc.testTools import parseXML, DataFilesHandler from fontTools.ttLib import TTFont import copy -import os -import sys import unittest from io import BytesIO @@ -119,9 +125,18 @@ def test_unique_glyph_names(self): glyphOrder = font2.getGlyphOrder() self.assertEqual(len(glyphOrder), len(set(glyphOrder))) + def test_reading_supplement_encoding(self): + cff_path = self.getpath("TestSupplementEncoding.cff") + topDict = None + with open(cff_path, "rb") as fontfile: + cff = CFFFontSet() + cff.decompile(fontfile, None) + topDict = cff[0] + self.assertEqual(topDict.Encoding[9], "space") + self.assertEqual(topDict.Encoding[32], "space") -class CFFToCFF2Test(DataFilesHandler): +class CFFToCFF2Test(DataFilesHandler): def test_conversion(self): font_path = self.getpath("CFFToCFF2-1.otf") font = TTFont(font_path) diff --git a/Tests/cffLib/data/TestSupplementEncoding.cff b/Tests/cffLib/data/TestSupplementEncoding.cff new file mode 100644 index 0000000000000000000000000000000000000000..0e43582f004ba32221f71a5ae73e938a62b89e5d GIT binary patch literal 1930 zcmY+ETW}j!8OK*{cC~4O>X1m-yJ0tN2U^Gj)G(C7OoqYGWZF{EW=cqD+Bisd+*;Pn zx=WTEh%m+r?nE2IMe~+G^H6jQ#wp3!vIq}&2SmY1N4Ej&QS_$ zhX*(h=i$tE`Tu|CoX>=8>q1N>)9!CS{@p{59oql&fNNmv@W}z6dvx>obNYQdZQp{f zTTg5>QEJ}w?^CDl>^sfi3*Dgq9&mD(xYwNiB!ZhvpXj;w{?GS){m~(xb70Krb9w^< zBc1`@*zmxJV|Z}TA8;A9hux!|VUN%42>fttcyw@hba*T>;`IBsqW`x$!kD)s=eDmS zH@iMUbmSId+J~4BL)0`^rfE=R|+fTHr3YVYcG=v^O2!4tqn;7KOmlEk0*wxp?EOj zB|X7vJ3(7?OkcM`6ifROo}fG69t(PtIJB9ynW{J^&&g$3R`Jd@VAI#lmcQ%Yw@#PD zmt{&`Xy)fhr8LQFw8m>0HLbG6l*r)X#7uIWi1JZ3La~vF=y(Kr;A)S)9eV?GqwoOs zOJJH^)LQCNud*aAEfEV`lW9;)BUTGlLKUy-D&mssrTmMeKakCY=};!hCKw!IW(z>| z!`7T4Hj0$gSjaDujoA#N(Iuu38za1_ScEk?26-PD*pCTXIaeVU8eFnQ&nD$)fN-;+ zSdvP{f}EFhkLHyTdPIp!0`Vf;B{DVURi-!fN;`9b{KZgDFzTf|%8 z)xjmI<$SL46xsL_3i_}!|3u*`_9(o#BA|jat&r!JQ?VA^idKSNVrbm!Pf&56D?LmG zg1LNz4(BKGlOhhg%@+N6{m)i|JnV-lbm(a0nWu@DM3?HORCnEb)`$Dft|VV4e|%AC zw&`}G-jdJZ^11v{lXyM6GSs4mS|_T{3*aH(?1A;pgVuVXR;f_sa!sm}jT)P%(B-HU z4iJ7Wnlu0>nRF_P^9(b^kjz9*VCV@UmFEh0tiskBgayp$4lA^G94NRC1H8VDLLEB> zyHRi)v*2_-sjJB!72jUO-(Gz6>U+c$b~#?B;`MknuBin@Q8Zl9#M9@9U%6K8rz!YE z=MgJkoRSqn&dGv83G!wTt|%38%T~Nhmt#^SNch=sJWj>qk#vA$n4B>2Uq7tGNm&`s zfo1Dlp{i+=rd10yvQ|wej4zQWM8iZN9Ze*tL?X(C$xy7ICFq2fuFesb-`cBZz3DtRVw+SSAxt`q7}^4LIf%t1T+zES2RK5;TL;vt@yJ#o%OwOyBD zukr99=*LIJ{Z0kmvKz{N==C+Ca3LQxXs9B{##J>W|i{0s;-#kw*-+dDt2)uee`nim#Y z1bD!#X61}TaS|tR)A%%(p5{n+FYLbl9W2a74PGfG$;LA98=%w!`>=n&2ZkA6IOge% zxmkaZ;7_r+B()5`iXMgC;04U`@S5=`!0C>|m@|%_{TdS{jmJw(ioBR9a3YuIa(FJy z#G)^`sh-`t8aEXZueiHt{@V9FC zv%IEr2D>eCQR7