1*e1fe3e4aSElliott Hughesfrom fontTools.config import OPTIONS 2*e1fe3e4aSElliott Hughesfrom fontTools.misc.textTools import Tag, bytesjoin 3*e1fe3e4aSElliott Hughesfrom .DefaultTable import DefaultTable 4*e1fe3e4aSElliott Hughesfrom enum import IntEnum 5*e1fe3e4aSElliott Hughesimport sys 6*e1fe3e4aSElliott Hughesimport array 7*e1fe3e4aSElliott Hughesimport struct 8*e1fe3e4aSElliott Hughesimport logging 9*e1fe3e4aSElliott Hughesfrom functools import lru_cache 10*e1fe3e4aSElliott Hughesfrom typing import Iterator, NamedTuple, Optional, Tuple 11*e1fe3e4aSElliott Hughes 12*e1fe3e4aSElliott Hugheslog = logging.getLogger(__name__) 13*e1fe3e4aSElliott Hughes 14*e1fe3e4aSElliott Hugheshave_uharfbuzz = False 15*e1fe3e4aSElliott Hughestry: 16*e1fe3e4aSElliott Hughes import uharfbuzz as hb 17*e1fe3e4aSElliott Hughes 18*e1fe3e4aSElliott Hughes # repack method added in uharfbuzz >= 0.23; if uharfbuzz *can* be 19*e1fe3e4aSElliott Hughes # imported but repack method is missing, behave as if uharfbuzz 20*e1fe3e4aSElliott Hughes # is not available (fallback to the slower Python implementation) 21*e1fe3e4aSElliott Hughes have_uharfbuzz = callable(getattr(hb, "repack", None)) 22*e1fe3e4aSElliott Hughesexcept ImportError: 23*e1fe3e4aSElliott Hughes pass 24*e1fe3e4aSElliott Hughes 25*e1fe3e4aSElliott HughesUSE_HARFBUZZ_REPACKER = OPTIONS[f"{__name__}:USE_HARFBUZZ_REPACKER"] 26*e1fe3e4aSElliott Hughes 27*e1fe3e4aSElliott Hughes 28*e1fe3e4aSElliott Hughesclass OverflowErrorRecord(object): 29*e1fe3e4aSElliott Hughes def __init__(self, overflowTuple): 30*e1fe3e4aSElliott Hughes self.tableType = overflowTuple[0] 31*e1fe3e4aSElliott Hughes self.LookupListIndex = overflowTuple[1] 32*e1fe3e4aSElliott Hughes self.SubTableIndex = overflowTuple[2] 33*e1fe3e4aSElliott Hughes self.itemName = overflowTuple[3] 34*e1fe3e4aSElliott Hughes self.itemIndex = overflowTuple[4] 35*e1fe3e4aSElliott Hughes 36*e1fe3e4aSElliott Hughes def __repr__(self): 37*e1fe3e4aSElliott Hughes return str( 38*e1fe3e4aSElliott Hughes ( 39*e1fe3e4aSElliott Hughes self.tableType, 40*e1fe3e4aSElliott Hughes "LookupIndex:", 41*e1fe3e4aSElliott Hughes self.LookupListIndex, 42*e1fe3e4aSElliott Hughes "SubTableIndex:", 43*e1fe3e4aSElliott Hughes self.SubTableIndex, 44*e1fe3e4aSElliott Hughes "ItemName:", 45*e1fe3e4aSElliott Hughes self.itemName, 46*e1fe3e4aSElliott Hughes "ItemIndex:", 47*e1fe3e4aSElliott Hughes self.itemIndex, 48*e1fe3e4aSElliott Hughes ) 49*e1fe3e4aSElliott Hughes ) 50*e1fe3e4aSElliott Hughes 51*e1fe3e4aSElliott Hughes 52*e1fe3e4aSElliott Hughesclass OTLOffsetOverflowError(Exception): 53*e1fe3e4aSElliott Hughes def __init__(self, overflowErrorRecord): 54*e1fe3e4aSElliott Hughes self.value = overflowErrorRecord 55*e1fe3e4aSElliott Hughes 56*e1fe3e4aSElliott Hughes def __str__(self): 57*e1fe3e4aSElliott Hughes return repr(self.value) 58*e1fe3e4aSElliott Hughes 59*e1fe3e4aSElliott Hughes 60*e1fe3e4aSElliott Hughesclass RepackerState(IntEnum): 61*e1fe3e4aSElliott Hughes # Repacking control flow is implemnted using a state machine. The state machine table: 62*e1fe3e4aSElliott Hughes # 63*e1fe3e4aSElliott Hughes # State | Packing Success | Packing Failed | Exception Raised | 64*e1fe3e4aSElliott Hughes # ------------+-----------------+----------------+------------------+ 65*e1fe3e4aSElliott Hughes # PURE_FT | Return result | PURE_FT | Return failure | 66*e1fe3e4aSElliott Hughes # HB_FT | Return result | HB_FT | FT_FALLBACK | 67*e1fe3e4aSElliott Hughes # FT_FALLBACK | HB_FT | FT_FALLBACK | Return failure | 68*e1fe3e4aSElliott Hughes 69*e1fe3e4aSElliott Hughes # Pack only with fontTools, don't allow sharing between extensions. 70*e1fe3e4aSElliott Hughes PURE_FT = 1 71*e1fe3e4aSElliott Hughes 72*e1fe3e4aSElliott Hughes # Attempt to pack with harfbuzz (allowing sharing between extensions) 73*e1fe3e4aSElliott Hughes # use fontTools to attempt overflow resolution. 74*e1fe3e4aSElliott Hughes HB_FT = 2 75*e1fe3e4aSElliott Hughes 76*e1fe3e4aSElliott Hughes # Fallback if HB/FT packing gets stuck. Pack only with fontTools, don't allow sharing between 77*e1fe3e4aSElliott Hughes # extensions. 78*e1fe3e4aSElliott Hughes FT_FALLBACK = 3 79*e1fe3e4aSElliott Hughes 80*e1fe3e4aSElliott Hughes 81*e1fe3e4aSElliott Hughesclass BaseTTXConverter(DefaultTable): 82*e1fe3e4aSElliott Hughes """Generic base class for TTX table converters. It functions as an 83*e1fe3e4aSElliott Hughes adapter between the TTX (ttLib actually) table model and the model 84*e1fe3e4aSElliott Hughes we use for OpenType tables, which is necessarily subtly different. 85*e1fe3e4aSElliott Hughes """ 86*e1fe3e4aSElliott Hughes 87*e1fe3e4aSElliott Hughes def decompile(self, data, font): 88*e1fe3e4aSElliott Hughes """Create an object from the binary data. Called automatically on access.""" 89*e1fe3e4aSElliott Hughes from . import otTables 90*e1fe3e4aSElliott Hughes 91*e1fe3e4aSElliott Hughes reader = OTTableReader(data, tableTag=self.tableTag) 92*e1fe3e4aSElliott Hughes tableClass = getattr(otTables, self.tableTag) 93*e1fe3e4aSElliott Hughes self.table = tableClass() 94*e1fe3e4aSElliott Hughes self.table.decompile(reader, font) 95*e1fe3e4aSElliott Hughes 96*e1fe3e4aSElliott Hughes def compile(self, font): 97*e1fe3e4aSElliott Hughes """Compiles the table into binary. Called automatically on save.""" 98*e1fe3e4aSElliott Hughes 99*e1fe3e4aSElliott Hughes # General outline: 100*e1fe3e4aSElliott Hughes # Create a top-level OTTableWriter for the GPOS/GSUB table. 101*e1fe3e4aSElliott Hughes # Call the compile method for the the table 102*e1fe3e4aSElliott Hughes # for each 'converter' record in the table converter list 103*e1fe3e4aSElliott Hughes # call converter's write method for each item in the value. 104*e1fe3e4aSElliott Hughes # - For simple items, the write method adds a string to the 105*e1fe3e4aSElliott Hughes # writer's self.items list. 106*e1fe3e4aSElliott Hughes # - For Struct/Table/Subtable items, it add first adds new writer to the 107*e1fe3e4aSElliott Hughes # to the writer's self.items, then calls the item's compile method. 108*e1fe3e4aSElliott Hughes # This creates a tree of writers, rooted at the GUSB/GPOS writer, with 109*e1fe3e4aSElliott Hughes # each writer representing a table, and the writer.items list containing 110*e1fe3e4aSElliott Hughes # the child data strings and writers. 111*e1fe3e4aSElliott Hughes # call the getAllData method 112*e1fe3e4aSElliott Hughes # call _doneWriting, which removes duplicates 113*e1fe3e4aSElliott Hughes # call _gatherTables. This traverses the tables, adding unique occurences to a flat list of tables 114*e1fe3e4aSElliott Hughes # Traverse the flat list of tables, calling getDataLength on each to update their position 115*e1fe3e4aSElliott Hughes # Traverse the flat list of tables again, calling getData each get the data in the table, now that 116*e1fe3e4aSElliott Hughes # pos's and offset are known. 117*e1fe3e4aSElliott Hughes 118*e1fe3e4aSElliott Hughes # If a lookup subtable overflows an offset, we have to start all over. 119*e1fe3e4aSElliott Hughes overflowRecord = None 120*e1fe3e4aSElliott Hughes # this is 3-state option: default (None) means automatically use hb.repack or 121*e1fe3e4aSElliott Hughes # silently fall back if it fails; True, use it and raise error if not possible 122*e1fe3e4aSElliott Hughes # or it errors out; False, don't use it, even if you can. 123*e1fe3e4aSElliott Hughes use_hb_repack = font.cfg[USE_HARFBUZZ_REPACKER] 124*e1fe3e4aSElliott Hughes if self.tableTag in ("GSUB", "GPOS"): 125*e1fe3e4aSElliott Hughes if use_hb_repack is False: 126*e1fe3e4aSElliott Hughes log.debug( 127*e1fe3e4aSElliott Hughes "hb.repack disabled, compiling '%s' with pure-python serializer", 128*e1fe3e4aSElliott Hughes self.tableTag, 129*e1fe3e4aSElliott Hughes ) 130*e1fe3e4aSElliott Hughes elif not have_uharfbuzz: 131*e1fe3e4aSElliott Hughes if use_hb_repack is True: 132*e1fe3e4aSElliott Hughes raise ImportError("No module named 'uharfbuzz'") 133*e1fe3e4aSElliott Hughes else: 134*e1fe3e4aSElliott Hughes assert use_hb_repack is None 135*e1fe3e4aSElliott Hughes log.debug( 136*e1fe3e4aSElliott Hughes "uharfbuzz not found, compiling '%s' with pure-python serializer", 137*e1fe3e4aSElliott Hughes self.tableTag, 138*e1fe3e4aSElliott Hughes ) 139*e1fe3e4aSElliott Hughes 140*e1fe3e4aSElliott Hughes if ( 141*e1fe3e4aSElliott Hughes use_hb_repack in (None, True) 142*e1fe3e4aSElliott Hughes and have_uharfbuzz 143*e1fe3e4aSElliott Hughes and self.tableTag in ("GSUB", "GPOS") 144*e1fe3e4aSElliott Hughes ): 145*e1fe3e4aSElliott Hughes state = RepackerState.HB_FT 146*e1fe3e4aSElliott Hughes else: 147*e1fe3e4aSElliott Hughes state = RepackerState.PURE_FT 148*e1fe3e4aSElliott Hughes 149*e1fe3e4aSElliott Hughes hb_first_error_logged = False 150*e1fe3e4aSElliott Hughes lastOverflowRecord = None 151*e1fe3e4aSElliott Hughes while True: 152*e1fe3e4aSElliott Hughes try: 153*e1fe3e4aSElliott Hughes writer = OTTableWriter(tableTag=self.tableTag) 154*e1fe3e4aSElliott Hughes self.table.compile(writer, font) 155*e1fe3e4aSElliott Hughes if state == RepackerState.HB_FT: 156*e1fe3e4aSElliott Hughes return self.tryPackingHarfbuzz(writer, hb_first_error_logged) 157*e1fe3e4aSElliott Hughes elif state == RepackerState.PURE_FT: 158*e1fe3e4aSElliott Hughes return self.tryPackingFontTools(writer) 159*e1fe3e4aSElliott Hughes elif state == RepackerState.FT_FALLBACK: 160*e1fe3e4aSElliott Hughes # Run packing with FontTools only, but don't return the result as it will 161*e1fe3e4aSElliott Hughes # not be optimally packed. Once a successful packing has been found, state is 162*e1fe3e4aSElliott Hughes # changed back to harfbuzz packing to produce the final, optimal, packing. 163*e1fe3e4aSElliott Hughes self.tryPackingFontTools(writer) 164*e1fe3e4aSElliott Hughes log.debug( 165*e1fe3e4aSElliott Hughes "Re-enabling sharing between extensions and switching back to " 166*e1fe3e4aSElliott Hughes "harfbuzz+fontTools packing." 167*e1fe3e4aSElliott Hughes ) 168*e1fe3e4aSElliott Hughes state = RepackerState.HB_FT 169*e1fe3e4aSElliott Hughes 170*e1fe3e4aSElliott Hughes except OTLOffsetOverflowError as e: 171*e1fe3e4aSElliott Hughes hb_first_error_logged = True 172*e1fe3e4aSElliott Hughes ok = self.tryResolveOverflow(font, e, lastOverflowRecord) 173*e1fe3e4aSElliott Hughes lastOverflowRecord = e.value 174*e1fe3e4aSElliott Hughes 175*e1fe3e4aSElliott Hughes if ok: 176*e1fe3e4aSElliott Hughes continue 177*e1fe3e4aSElliott Hughes 178*e1fe3e4aSElliott Hughes if state is RepackerState.HB_FT: 179*e1fe3e4aSElliott Hughes log.debug( 180*e1fe3e4aSElliott Hughes "Harfbuzz packing out of resolutions, disabling sharing between extensions and " 181*e1fe3e4aSElliott Hughes "switching to fontTools only packing." 182*e1fe3e4aSElliott Hughes ) 183*e1fe3e4aSElliott Hughes state = RepackerState.FT_FALLBACK 184*e1fe3e4aSElliott Hughes else: 185*e1fe3e4aSElliott Hughes raise 186*e1fe3e4aSElliott Hughes 187*e1fe3e4aSElliott Hughes def tryPackingHarfbuzz(self, writer, hb_first_error_logged): 188*e1fe3e4aSElliott Hughes try: 189*e1fe3e4aSElliott Hughes log.debug("serializing '%s' with hb.repack", self.tableTag) 190*e1fe3e4aSElliott Hughes return writer.getAllDataUsingHarfbuzz(self.tableTag) 191*e1fe3e4aSElliott Hughes except (ValueError, MemoryError, hb.RepackerError) as e: 192*e1fe3e4aSElliott Hughes # Only log hb repacker errors the first time they occur in 193*e1fe3e4aSElliott Hughes # the offset-overflow resolution loop, they are just noisy. 194*e1fe3e4aSElliott Hughes # Maybe we can revisit this if/when uharfbuzz actually gives 195*e1fe3e4aSElliott Hughes # us more info as to why hb.repack failed... 196*e1fe3e4aSElliott Hughes if not hb_first_error_logged: 197*e1fe3e4aSElliott Hughes error_msg = f"{type(e).__name__}" 198*e1fe3e4aSElliott Hughes if str(e) != "": 199*e1fe3e4aSElliott Hughes error_msg += f": {e}" 200*e1fe3e4aSElliott Hughes log.warning( 201*e1fe3e4aSElliott Hughes "hb.repack failed to serialize '%s', attempting fonttools resolutions " 202*e1fe3e4aSElliott Hughes "; the error message was: %s", 203*e1fe3e4aSElliott Hughes self.tableTag, 204*e1fe3e4aSElliott Hughes error_msg, 205*e1fe3e4aSElliott Hughes ) 206*e1fe3e4aSElliott Hughes hb_first_error_logged = True 207*e1fe3e4aSElliott Hughes return writer.getAllData(remove_duplicate=False) 208*e1fe3e4aSElliott Hughes 209*e1fe3e4aSElliott Hughes def tryPackingFontTools(self, writer): 210*e1fe3e4aSElliott Hughes return writer.getAllData() 211*e1fe3e4aSElliott Hughes 212*e1fe3e4aSElliott Hughes def tryResolveOverflow(self, font, e, lastOverflowRecord): 213*e1fe3e4aSElliott Hughes ok = 0 214*e1fe3e4aSElliott Hughes if lastOverflowRecord == e.value: 215*e1fe3e4aSElliott Hughes # Oh well... 216*e1fe3e4aSElliott Hughes return ok 217*e1fe3e4aSElliott Hughes 218*e1fe3e4aSElliott Hughes overflowRecord = e.value 219*e1fe3e4aSElliott Hughes log.info("Attempting to fix OTLOffsetOverflowError %s", e) 220*e1fe3e4aSElliott Hughes 221*e1fe3e4aSElliott Hughes if overflowRecord.itemName is None: 222*e1fe3e4aSElliott Hughes from .otTables import fixLookupOverFlows 223*e1fe3e4aSElliott Hughes 224*e1fe3e4aSElliott Hughes ok = fixLookupOverFlows(font, overflowRecord) 225*e1fe3e4aSElliott Hughes else: 226*e1fe3e4aSElliott Hughes from .otTables import fixSubTableOverFlows 227*e1fe3e4aSElliott Hughes 228*e1fe3e4aSElliott Hughes ok = fixSubTableOverFlows(font, overflowRecord) 229*e1fe3e4aSElliott Hughes 230*e1fe3e4aSElliott Hughes if ok: 231*e1fe3e4aSElliott Hughes return ok 232*e1fe3e4aSElliott Hughes 233*e1fe3e4aSElliott Hughes # Try upgrading lookup to Extension and hope 234*e1fe3e4aSElliott Hughes # that cross-lookup sharing not happening would 235*e1fe3e4aSElliott Hughes # fix overflow... 236*e1fe3e4aSElliott Hughes from .otTables import fixLookupOverFlows 237*e1fe3e4aSElliott Hughes 238*e1fe3e4aSElliott Hughes return fixLookupOverFlows(font, overflowRecord) 239*e1fe3e4aSElliott Hughes 240*e1fe3e4aSElliott Hughes def toXML(self, writer, font): 241*e1fe3e4aSElliott Hughes self.table.toXML2(writer, font) 242*e1fe3e4aSElliott Hughes 243*e1fe3e4aSElliott Hughes def fromXML(self, name, attrs, content, font): 244*e1fe3e4aSElliott Hughes from . import otTables 245*e1fe3e4aSElliott Hughes 246*e1fe3e4aSElliott Hughes if not hasattr(self, "table"): 247*e1fe3e4aSElliott Hughes tableClass = getattr(otTables, self.tableTag) 248*e1fe3e4aSElliott Hughes self.table = tableClass() 249*e1fe3e4aSElliott Hughes self.table.fromXML(name, attrs, content, font) 250*e1fe3e4aSElliott Hughes self.table.populateDefaults() 251*e1fe3e4aSElliott Hughes 252*e1fe3e4aSElliott Hughes def ensureDecompiled(self, recurse=True): 253*e1fe3e4aSElliott Hughes self.table.ensureDecompiled(recurse=recurse) 254*e1fe3e4aSElliott Hughes 255*e1fe3e4aSElliott Hughes 256*e1fe3e4aSElliott Hughes# https://github.com/fonttools/fonttools/pull/2285#issuecomment-834652928 257*e1fe3e4aSElliott Hughesassert len(struct.pack("i", 0)) == 4 258*e1fe3e4aSElliott Hughesassert array.array("i").itemsize == 4, "Oops, file a bug against fonttools." 259*e1fe3e4aSElliott Hughes 260*e1fe3e4aSElliott Hughes 261*e1fe3e4aSElliott Hughesclass OTTableReader(object): 262*e1fe3e4aSElliott Hughes """Helper class to retrieve data from an OpenType table.""" 263*e1fe3e4aSElliott Hughes 264*e1fe3e4aSElliott Hughes __slots__ = ("data", "offset", "pos", "localState", "tableTag") 265*e1fe3e4aSElliott Hughes 266*e1fe3e4aSElliott Hughes def __init__(self, data, localState=None, offset=0, tableTag=None): 267*e1fe3e4aSElliott Hughes self.data = data 268*e1fe3e4aSElliott Hughes self.offset = offset 269*e1fe3e4aSElliott Hughes self.pos = offset 270*e1fe3e4aSElliott Hughes self.localState = localState 271*e1fe3e4aSElliott Hughes self.tableTag = tableTag 272*e1fe3e4aSElliott Hughes 273*e1fe3e4aSElliott Hughes def advance(self, count): 274*e1fe3e4aSElliott Hughes self.pos += count 275*e1fe3e4aSElliott Hughes 276*e1fe3e4aSElliott Hughes def seek(self, pos): 277*e1fe3e4aSElliott Hughes self.pos = pos 278*e1fe3e4aSElliott Hughes 279*e1fe3e4aSElliott Hughes def copy(self): 280*e1fe3e4aSElliott Hughes other = self.__class__(self.data, self.localState, self.offset, self.tableTag) 281*e1fe3e4aSElliott Hughes other.pos = self.pos 282*e1fe3e4aSElliott Hughes return other 283*e1fe3e4aSElliott Hughes 284*e1fe3e4aSElliott Hughes def getSubReader(self, offset): 285*e1fe3e4aSElliott Hughes offset = self.offset + offset 286*e1fe3e4aSElliott Hughes return self.__class__(self.data, self.localState, offset, self.tableTag) 287*e1fe3e4aSElliott Hughes 288*e1fe3e4aSElliott Hughes def readValue(self, typecode, staticSize): 289*e1fe3e4aSElliott Hughes pos = self.pos 290*e1fe3e4aSElliott Hughes newpos = pos + staticSize 291*e1fe3e4aSElliott Hughes (value,) = struct.unpack(f">{typecode}", self.data[pos:newpos]) 292*e1fe3e4aSElliott Hughes self.pos = newpos 293*e1fe3e4aSElliott Hughes return value 294*e1fe3e4aSElliott Hughes 295*e1fe3e4aSElliott Hughes def readArray(self, typecode, staticSize, count): 296*e1fe3e4aSElliott Hughes pos = self.pos 297*e1fe3e4aSElliott Hughes newpos = pos + count * staticSize 298*e1fe3e4aSElliott Hughes value = array.array(typecode, self.data[pos:newpos]) 299*e1fe3e4aSElliott Hughes if sys.byteorder != "big": 300*e1fe3e4aSElliott Hughes value.byteswap() 301*e1fe3e4aSElliott Hughes self.pos = newpos 302*e1fe3e4aSElliott Hughes return value.tolist() 303*e1fe3e4aSElliott Hughes 304*e1fe3e4aSElliott Hughes def readInt8(self): 305*e1fe3e4aSElliott Hughes return self.readValue("b", staticSize=1) 306*e1fe3e4aSElliott Hughes 307*e1fe3e4aSElliott Hughes def readInt8Array(self, count): 308*e1fe3e4aSElliott Hughes return self.readArray("b", staticSize=1, count=count) 309*e1fe3e4aSElliott Hughes 310*e1fe3e4aSElliott Hughes def readShort(self): 311*e1fe3e4aSElliott Hughes return self.readValue("h", staticSize=2) 312*e1fe3e4aSElliott Hughes 313*e1fe3e4aSElliott Hughes def readShortArray(self, count): 314*e1fe3e4aSElliott Hughes return self.readArray("h", staticSize=2, count=count) 315*e1fe3e4aSElliott Hughes 316*e1fe3e4aSElliott Hughes def readLong(self): 317*e1fe3e4aSElliott Hughes return self.readValue("i", staticSize=4) 318*e1fe3e4aSElliott Hughes 319*e1fe3e4aSElliott Hughes def readLongArray(self, count): 320*e1fe3e4aSElliott Hughes return self.readArray("i", staticSize=4, count=count) 321*e1fe3e4aSElliott Hughes 322*e1fe3e4aSElliott Hughes def readUInt8(self): 323*e1fe3e4aSElliott Hughes return self.readValue("B", staticSize=1) 324*e1fe3e4aSElliott Hughes 325*e1fe3e4aSElliott Hughes def readUInt8Array(self, count): 326*e1fe3e4aSElliott Hughes return self.readArray("B", staticSize=1, count=count) 327*e1fe3e4aSElliott Hughes 328*e1fe3e4aSElliott Hughes def readUShort(self): 329*e1fe3e4aSElliott Hughes return self.readValue("H", staticSize=2) 330*e1fe3e4aSElliott Hughes 331*e1fe3e4aSElliott Hughes def readUShortArray(self, count): 332*e1fe3e4aSElliott Hughes return self.readArray("H", staticSize=2, count=count) 333*e1fe3e4aSElliott Hughes 334*e1fe3e4aSElliott Hughes def readULong(self): 335*e1fe3e4aSElliott Hughes return self.readValue("I", staticSize=4) 336*e1fe3e4aSElliott Hughes 337*e1fe3e4aSElliott Hughes def readULongArray(self, count): 338*e1fe3e4aSElliott Hughes return self.readArray("I", staticSize=4, count=count) 339*e1fe3e4aSElliott Hughes 340*e1fe3e4aSElliott Hughes def readUInt24(self): 341*e1fe3e4aSElliott Hughes pos = self.pos 342*e1fe3e4aSElliott Hughes newpos = pos + 3 343*e1fe3e4aSElliott Hughes (value,) = struct.unpack(">l", b"\0" + self.data[pos:newpos]) 344*e1fe3e4aSElliott Hughes self.pos = newpos 345*e1fe3e4aSElliott Hughes return value 346*e1fe3e4aSElliott Hughes 347*e1fe3e4aSElliott Hughes def readUInt24Array(self, count): 348*e1fe3e4aSElliott Hughes return [self.readUInt24() for _ in range(count)] 349*e1fe3e4aSElliott Hughes 350*e1fe3e4aSElliott Hughes def readTag(self): 351*e1fe3e4aSElliott Hughes pos = self.pos 352*e1fe3e4aSElliott Hughes newpos = pos + 4 353*e1fe3e4aSElliott Hughes value = Tag(self.data[pos:newpos]) 354*e1fe3e4aSElliott Hughes assert len(value) == 4, value 355*e1fe3e4aSElliott Hughes self.pos = newpos 356*e1fe3e4aSElliott Hughes return value 357*e1fe3e4aSElliott Hughes 358*e1fe3e4aSElliott Hughes def readData(self, count): 359*e1fe3e4aSElliott Hughes pos = self.pos 360*e1fe3e4aSElliott Hughes newpos = pos + count 361*e1fe3e4aSElliott Hughes value = self.data[pos:newpos] 362*e1fe3e4aSElliott Hughes self.pos = newpos 363*e1fe3e4aSElliott Hughes return value 364*e1fe3e4aSElliott Hughes 365*e1fe3e4aSElliott Hughes def __setitem__(self, name, value): 366*e1fe3e4aSElliott Hughes state = self.localState.copy() if self.localState else dict() 367*e1fe3e4aSElliott Hughes state[name] = value 368*e1fe3e4aSElliott Hughes self.localState = state 369*e1fe3e4aSElliott Hughes 370*e1fe3e4aSElliott Hughes def __getitem__(self, name): 371*e1fe3e4aSElliott Hughes return self.localState and self.localState[name] 372*e1fe3e4aSElliott Hughes 373*e1fe3e4aSElliott Hughes def __contains__(self, name): 374*e1fe3e4aSElliott Hughes return self.localState and name in self.localState 375*e1fe3e4aSElliott Hughes 376*e1fe3e4aSElliott Hughes 377*e1fe3e4aSElliott Hughesclass OffsetToWriter(object): 378*e1fe3e4aSElliott Hughes def __init__(self, subWriter, offsetSize): 379*e1fe3e4aSElliott Hughes self.subWriter = subWriter 380*e1fe3e4aSElliott Hughes self.offsetSize = offsetSize 381*e1fe3e4aSElliott Hughes 382*e1fe3e4aSElliott Hughes def __eq__(self, other): 383*e1fe3e4aSElliott Hughes if type(self) != type(other): 384*e1fe3e4aSElliott Hughes return NotImplemented 385*e1fe3e4aSElliott Hughes return self.subWriter == other.subWriter and self.offsetSize == other.offsetSize 386*e1fe3e4aSElliott Hughes 387*e1fe3e4aSElliott Hughes def __hash__(self): 388*e1fe3e4aSElliott Hughes # only works after self._doneWriting() has been called 389*e1fe3e4aSElliott Hughes return hash((self.subWriter, self.offsetSize)) 390*e1fe3e4aSElliott Hughes 391*e1fe3e4aSElliott Hughes 392*e1fe3e4aSElliott Hughesclass OTTableWriter(object): 393*e1fe3e4aSElliott Hughes """Helper class to gather and assemble data for OpenType tables.""" 394*e1fe3e4aSElliott Hughes 395*e1fe3e4aSElliott Hughes def __init__(self, localState=None, tableTag=None): 396*e1fe3e4aSElliott Hughes self.items = [] 397*e1fe3e4aSElliott Hughes self.pos = None 398*e1fe3e4aSElliott Hughes self.localState = localState 399*e1fe3e4aSElliott Hughes self.tableTag = tableTag 400*e1fe3e4aSElliott Hughes self.parent = None 401*e1fe3e4aSElliott Hughes 402*e1fe3e4aSElliott Hughes def __setitem__(self, name, value): 403*e1fe3e4aSElliott Hughes state = self.localState.copy() if self.localState else dict() 404*e1fe3e4aSElliott Hughes state[name] = value 405*e1fe3e4aSElliott Hughes self.localState = state 406*e1fe3e4aSElliott Hughes 407*e1fe3e4aSElliott Hughes def __getitem__(self, name): 408*e1fe3e4aSElliott Hughes return self.localState[name] 409*e1fe3e4aSElliott Hughes 410*e1fe3e4aSElliott Hughes def __delitem__(self, name): 411*e1fe3e4aSElliott Hughes del self.localState[name] 412*e1fe3e4aSElliott Hughes 413*e1fe3e4aSElliott Hughes # assembler interface 414*e1fe3e4aSElliott Hughes 415*e1fe3e4aSElliott Hughes def getDataLength(self): 416*e1fe3e4aSElliott Hughes """Return the length of this table in bytes, without subtables.""" 417*e1fe3e4aSElliott Hughes l = 0 418*e1fe3e4aSElliott Hughes for item in self.items: 419*e1fe3e4aSElliott Hughes if hasattr(item, "getCountData"): 420*e1fe3e4aSElliott Hughes l += item.size 421*e1fe3e4aSElliott Hughes elif hasattr(item, "subWriter"): 422*e1fe3e4aSElliott Hughes l += item.offsetSize 423*e1fe3e4aSElliott Hughes else: 424*e1fe3e4aSElliott Hughes l = l + len(item) 425*e1fe3e4aSElliott Hughes return l 426*e1fe3e4aSElliott Hughes 427*e1fe3e4aSElliott Hughes def getData(self): 428*e1fe3e4aSElliott Hughes """Assemble the data for this writer/table, without subtables.""" 429*e1fe3e4aSElliott Hughes items = list(self.items) # make a shallow copy 430*e1fe3e4aSElliott Hughes pos = self.pos 431*e1fe3e4aSElliott Hughes numItems = len(items) 432*e1fe3e4aSElliott Hughes for i in range(numItems): 433*e1fe3e4aSElliott Hughes item = items[i] 434*e1fe3e4aSElliott Hughes 435*e1fe3e4aSElliott Hughes if hasattr(item, "subWriter"): 436*e1fe3e4aSElliott Hughes if item.offsetSize == 4: 437*e1fe3e4aSElliott Hughes items[i] = packULong(item.subWriter.pos - pos) 438*e1fe3e4aSElliott Hughes elif item.offsetSize == 2: 439*e1fe3e4aSElliott Hughes try: 440*e1fe3e4aSElliott Hughes items[i] = packUShort(item.subWriter.pos - pos) 441*e1fe3e4aSElliott Hughes except struct.error: 442*e1fe3e4aSElliott Hughes # provide data to fix overflow problem. 443*e1fe3e4aSElliott Hughes overflowErrorRecord = self.getOverflowErrorRecord( 444*e1fe3e4aSElliott Hughes item.subWriter 445*e1fe3e4aSElliott Hughes ) 446*e1fe3e4aSElliott Hughes 447*e1fe3e4aSElliott Hughes raise OTLOffsetOverflowError(overflowErrorRecord) 448*e1fe3e4aSElliott Hughes elif item.offsetSize == 3: 449*e1fe3e4aSElliott Hughes items[i] = packUInt24(item.subWriter.pos - pos) 450*e1fe3e4aSElliott Hughes else: 451*e1fe3e4aSElliott Hughes raise ValueError(item.offsetSize) 452*e1fe3e4aSElliott Hughes 453*e1fe3e4aSElliott Hughes return bytesjoin(items) 454*e1fe3e4aSElliott Hughes 455*e1fe3e4aSElliott Hughes def getDataForHarfbuzz(self): 456*e1fe3e4aSElliott Hughes """Assemble the data for this writer/table with all offset field set to 0""" 457*e1fe3e4aSElliott Hughes items = list(self.items) 458*e1fe3e4aSElliott Hughes packFuncs = {2: packUShort, 3: packUInt24, 4: packULong} 459*e1fe3e4aSElliott Hughes for i, item in enumerate(items): 460*e1fe3e4aSElliott Hughes if hasattr(item, "subWriter"): 461*e1fe3e4aSElliott Hughes # Offset value is not needed in harfbuzz repacker, so setting offset to 0 to avoid overflow here 462*e1fe3e4aSElliott Hughes if item.offsetSize in packFuncs: 463*e1fe3e4aSElliott Hughes items[i] = packFuncs[item.offsetSize](0) 464*e1fe3e4aSElliott Hughes else: 465*e1fe3e4aSElliott Hughes raise ValueError(item.offsetSize) 466*e1fe3e4aSElliott Hughes 467*e1fe3e4aSElliott Hughes return bytesjoin(items) 468*e1fe3e4aSElliott Hughes 469*e1fe3e4aSElliott Hughes def __hash__(self): 470*e1fe3e4aSElliott Hughes # only works after self._doneWriting() has been called 471*e1fe3e4aSElliott Hughes return hash(self.items) 472*e1fe3e4aSElliott Hughes 473*e1fe3e4aSElliott Hughes def __ne__(self, other): 474*e1fe3e4aSElliott Hughes result = self.__eq__(other) 475*e1fe3e4aSElliott Hughes return result if result is NotImplemented else not result 476*e1fe3e4aSElliott Hughes 477*e1fe3e4aSElliott Hughes def __eq__(self, other): 478*e1fe3e4aSElliott Hughes if type(self) != type(other): 479*e1fe3e4aSElliott Hughes return NotImplemented 480*e1fe3e4aSElliott Hughes return self.items == other.items 481*e1fe3e4aSElliott Hughes 482*e1fe3e4aSElliott Hughes def _doneWriting(self, internedTables, shareExtension=False): 483*e1fe3e4aSElliott Hughes # Convert CountData references to data string items 484*e1fe3e4aSElliott Hughes # collapse duplicate table references to a unique entry 485*e1fe3e4aSElliott Hughes # "tables" are OTTableWriter objects. 486*e1fe3e4aSElliott Hughes 487*e1fe3e4aSElliott Hughes # For Extension Lookup types, we can 488*e1fe3e4aSElliott Hughes # eliminate duplicates only within the tree under the Extension Lookup, 489*e1fe3e4aSElliott Hughes # as offsets may exceed 64K even between Extension LookupTable subtables. 490*e1fe3e4aSElliott Hughes isExtension = hasattr(self, "Extension") 491*e1fe3e4aSElliott Hughes 492*e1fe3e4aSElliott Hughes # Certain versions of Uniscribe reject the font if the GSUB/GPOS top-level 493*e1fe3e4aSElliott Hughes # arrays (ScriptList, FeatureList, LookupList) point to the same, possibly 494*e1fe3e4aSElliott Hughes # empty, array. So, we don't share those. 495*e1fe3e4aSElliott Hughes # See: https://github.com/fonttools/fonttools/issues/518 496*e1fe3e4aSElliott Hughes dontShare = hasattr(self, "DontShare") 497*e1fe3e4aSElliott Hughes 498*e1fe3e4aSElliott Hughes if isExtension and not shareExtension: 499*e1fe3e4aSElliott Hughes internedTables = {} 500*e1fe3e4aSElliott Hughes 501*e1fe3e4aSElliott Hughes items = self.items 502*e1fe3e4aSElliott Hughes for i in range(len(items)): 503*e1fe3e4aSElliott Hughes item = items[i] 504*e1fe3e4aSElliott Hughes if hasattr(item, "getCountData"): 505*e1fe3e4aSElliott Hughes items[i] = item.getCountData() 506*e1fe3e4aSElliott Hughes elif hasattr(item, "subWriter"): 507*e1fe3e4aSElliott Hughes item.subWriter._doneWriting( 508*e1fe3e4aSElliott Hughes internedTables, shareExtension=shareExtension 509*e1fe3e4aSElliott Hughes ) 510*e1fe3e4aSElliott Hughes # At this point, all subwriters are hashable based on their items. 511*e1fe3e4aSElliott Hughes # (See hash and comparison magic methods above.) So the ``setdefault`` 512*e1fe3e4aSElliott Hughes # call here will return the first writer object we've seen with 513*e1fe3e4aSElliott Hughes # equal content, or store it in the dictionary if it's not been 514*e1fe3e4aSElliott Hughes # seen yet. We therefore replace the subwriter object with an equivalent 515*e1fe3e4aSElliott Hughes # object, which deduplicates the tree. 516*e1fe3e4aSElliott Hughes if not dontShare: 517*e1fe3e4aSElliott Hughes items[i].subWriter = internedTables.setdefault( 518*e1fe3e4aSElliott Hughes item.subWriter, item.subWriter 519*e1fe3e4aSElliott Hughes ) 520*e1fe3e4aSElliott Hughes self.items = tuple(items) 521*e1fe3e4aSElliott Hughes 522*e1fe3e4aSElliott Hughes def _gatherTables(self, tables, extTables, done): 523*e1fe3e4aSElliott Hughes # Convert table references in self.items tree to a flat 524*e1fe3e4aSElliott Hughes # list of tables in depth-first traversal order. 525*e1fe3e4aSElliott Hughes # "tables" are OTTableWriter objects. 526*e1fe3e4aSElliott Hughes # We do the traversal in reverse order at each level, in order to 527*e1fe3e4aSElliott Hughes # resolve duplicate references to be the last reference in the list of tables. 528*e1fe3e4aSElliott Hughes # For extension lookups, duplicate references can be merged only within the 529*e1fe3e4aSElliott Hughes # writer tree under the extension lookup. 530*e1fe3e4aSElliott Hughes 531*e1fe3e4aSElliott Hughes done[id(self)] = True 532*e1fe3e4aSElliott Hughes 533*e1fe3e4aSElliott Hughes numItems = len(self.items) 534*e1fe3e4aSElliott Hughes iRange = list(range(numItems)) 535*e1fe3e4aSElliott Hughes iRange.reverse() 536*e1fe3e4aSElliott Hughes 537*e1fe3e4aSElliott Hughes isExtension = hasattr(self, "Extension") 538*e1fe3e4aSElliott Hughes 539*e1fe3e4aSElliott Hughes selfTables = tables 540*e1fe3e4aSElliott Hughes 541*e1fe3e4aSElliott Hughes if isExtension: 542*e1fe3e4aSElliott Hughes assert ( 543*e1fe3e4aSElliott Hughes extTables is not None 544*e1fe3e4aSElliott Hughes ), "Program or XML editing error. Extension subtables cannot contain extensions subtables" 545*e1fe3e4aSElliott Hughes tables, extTables, done = extTables, None, {} 546*e1fe3e4aSElliott Hughes 547*e1fe3e4aSElliott Hughes # add Coverage table if it is sorted last. 548*e1fe3e4aSElliott Hughes sortCoverageLast = False 549*e1fe3e4aSElliott Hughes if hasattr(self, "sortCoverageLast"): 550*e1fe3e4aSElliott Hughes # Find coverage table 551*e1fe3e4aSElliott Hughes for i in range(numItems): 552*e1fe3e4aSElliott Hughes item = self.items[i] 553*e1fe3e4aSElliott Hughes if ( 554*e1fe3e4aSElliott Hughes hasattr(item, "subWriter") 555*e1fe3e4aSElliott Hughes and getattr(item.subWriter, "name", None) == "Coverage" 556*e1fe3e4aSElliott Hughes ): 557*e1fe3e4aSElliott Hughes sortCoverageLast = True 558*e1fe3e4aSElliott Hughes break 559*e1fe3e4aSElliott Hughes if id(item.subWriter) not in done: 560*e1fe3e4aSElliott Hughes item.subWriter._gatherTables(tables, extTables, done) 561*e1fe3e4aSElliott Hughes else: 562*e1fe3e4aSElliott Hughes # We're a new parent of item 563*e1fe3e4aSElliott Hughes pass 564*e1fe3e4aSElliott Hughes 565*e1fe3e4aSElliott Hughes for i in iRange: 566*e1fe3e4aSElliott Hughes item = self.items[i] 567*e1fe3e4aSElliott Hughes if not hasattr(item, "subWriter"): 568*e1fe3e4aSElliott Hughes continue 569*e1fe3e4aSElliott Hughes 570*e1fe3e4aSElliott Hughes if ( 571*e1fe3e4aSElliott Hughes sortCoverageLast 572*e1fe3e4aSElliott Hughes and (i == 1) 573*e1fe3e4aSElliott Hughes and getattr(item.subWriter, "name", None) == "Coverage" 574*e1fe3e4aSElliott Hughes ): 575*e1fe3e4aSElliott Hughes # we've already 'gathered' it above 576*e1fe3e4aSElliott Hughes continue 577*e1fe3e4aSElliott Hughes 578*e1fe3e4aSElliott Hughes if id(item.subWriter) not in done: 579*e1fe3e4aSElliott Hughes item.subWriter._gatherTables(tables, extTables, done) 580*e1fe3e4aSElliott Hughes else: 581*e1fe3e4aSElliott Hughes # Item is already written out by other parent 582*e1fe3e4aSElliott Hughes pass 583*e1fe3e4aSElliott Hughes 584*e1fe3e4aSElliott Hughes selfTables.append(self) 585*e1fe3e4aSElliott Hughes 586*e1fe3e4aSElliott Hughes def _gatherGraphForHarfbuzz(self, tables, obj_list, done, objidx, virtual_edges): 587*e1fe3e4aSElliott Hughes real_links = [] 588*e1fe3e4aSElliott Hughes virtual_links = [] 589*e1fe3e4aSElliott Hughes item_idx = objidx 590*e1fe3e4aSElliott Hughes 591*e1fe3e4aSElliott Hughes # Merge virtual_links from parent 592*e1fe3e4aSElliott Hughes for idx in virtual_edges: 593*e1fe3e4aSElliott Hughes virtual_links.append((0, 0, idx)) 594*e1fe3e4aSElliott Hughes 595*e1fe3e4aSElliott Hughes sortCoverageLast = False 596*e1fe3e4aSElliott Hughes coverage_idx = 0 597*e1fe3e4aSElliott Hughes if hasattr(self, "sortCoverageLast"): 598*e1fe3e4aSElliott Hughes # Find coverage table 599*e1fe3e4aSElliott Hughes for i, item in enumerate(self.items): 600*e1fe3e4aSElliott Hughes if getattr(item, "name", None) == "Coverage": 601*e1fe3e4aSElliott Hughes sortCoverageLast = True 602*e1fe3e4aSElliott Hughes if id(item) not in done: 603*e1fe3e4aSElliott Hughes coverage_idx = item_idx = item._gatherGraphForHarfbuzz( 604*e1fe3e4aSElliott Hughes tables, obj_list, done, item_idx, virtual_edges 605*e1fe3e4aSElliott Hughes ) 606*e1fe3e4aSElliott Hughes else: 607*e1fe3e4aSElliott Hughes coverage_idx = done[id(item)] 608*e1fe3e4aSElliott Hughes virtual_edges.append(coverage_idx) 609*e1fe3e4aSElliott Hughes break 610*e1fe3e4aSElliott Hughes 611*e1fe3e4aSElliott Hughes child_idx = 0 612*e1fe3e4aSElliott Hughes offset_pos = 0 613*e1fe3e4aSElliott Hughes for i, item in enumerate(self.items): 614*e1fe3e4aSElliott Hughes if hasattr(item, "subWriter"): 615*e1fe3e4aSElliott Hughes pos = offset_pos 616*e1fe3e4aSElliott Hughes elif hasattr(item, "getCountData"): 617*e1fe3e4aSElliott Hughes offset_pos += item.size 618*e1fe3e4aSElliott Hughes continue 619*e1fe3e4aSElliott Hughes else: 620*e1fe3e4aSElliott Hughes offset_pos = offset_pos + len(item) 621*e1fe3e4aSElliott Hughes continue 622*e1fe3e4aSElliott Hughes 623*e1fe3e4aSElliott Hughes if id(item.subWriter) not in done: 624*e1fe3e4aSElliott Hughes child_idx = item_idx = item.subWriter._gatherGraphForHarfbuzz( 625*e1fe3e4aSElliott Hughes tables, obj_list, done, item_idx, virtual_edges 626*e1fe3e4aSElliott Hughes ) 627*e1fe3e4aSElliott Hughes else: 628*e1fe3e4aSElliott Hughes child_idx = done[id(item.subWriter)] 629*e1fe3e4aSElliott Hughes 630*e1fe3e4aSElliott Hughes real_edge = (pos, item.offsetSize, child_idx) 631*e1fe3e4aSElliott Hughes real_links.append(real_edge) 632*e1fe3e4aSElliott Hughes offset_pos += item.offsetSize 633*e1fe3e4aSElliott Hughes 634*e1fe3e4aSElliott Hughes tables.append(self) 635*e1fe3e4aSElliott Hughes obj_list.append((real_links, virtual_links)) 636*e1fe3e4aSElliott Hughes item_idx += 1 637*e1fe3e4aSElliott Hughes done[id(self)] = item_idx 638*e1fe3e4aSElliott Hughes if sortCoverageLast: 639*e1fe3e4aSElliott Hughes virtual_edges.pop() 640*e1fe3e4aSElliott Hughes 641*e1fe3e4aSElliott Hughes return item_idx 642*e1fe3e4aSElliott Hughes 643*e1fe3e4aSElliott Hughes def getAllDataUsingHarfbuzz(self, tableTag): 644*e1fe3e4aSElliott Hughes """The Whole table is represented as a Graph. 645*e1fe3e4aSElliott Hughes Assemble graph data and call Harfbuzz repacker to pack the table. 646*e1fe3e4aSElliott Hughes Harfbuzz repacker is faster and retain as much sub-table sharing as possible, see also: 647*e1fe3e4aSElliott Hughes https://github.com/harfbuzz/harfbuzz/blob/main/docs/repacker.md 648*e1fe3e4aSElliott Hughes The input format for hb.repack() method is explained here: 649*e1fe3e4aSElliott Hughes https://github.com/harfbuzz/uharfbuzz/blob/main/src/uharfbuzz/_harfbuzz.pyx#L1149 650*e1fe3e4aSElliott Hughes """ 651*e1fe3e4aSElliott Hughes internedTables = {} 652*e1fe3e4aSElliott Hughes self._doneWriting(internedTables, shareExtension=True) 653*e1fe3e4aSElliott Hughes tables = [] 654*e1fe3e4aSElliott Hughes obj_list = [] 655*e1fe3e4aSElliott Hughes done = {} 656*e1fe3e4aSElliott Hughes objidx = 0 657*e1fe3e4aSElliott Hughes virtual_edges = [] 658*e1fe3e4aSElliott Hughes self._gatherGraphForHarfbuzz(tables, obj_list, done, objidx, virtual_edges) 659*e1fe3e4aSElliott Hughes # Gather all data in two passes: the absolute positions of all 660*e1fe3e4aSElliott Hughes # subtable are needed before the actual data can be assembled. 661*e1fe3e4aSElliott Hughes pos = 0 662*e1fe3e4aSElliott Hughes for table in tables: 663*e1fe3e4aSElliott Hughes table.pos = pos 664*e1fe3e4aSElliott Hughes pos = pos + table.getDataLength() 665*e1fe3e4aSElliott Hughes 666*e1fe3e4aSElliott Hughes data = [] 667*e1fe3e4aSElliott Hughes for table in tables: 668*e1fe3e4aSElliott Hughes tableData = table.getDataForHarfbuzz() 669*e1fe3e4aSElliott Hughes data.append(tableData) 670*e1fe3e4aSElliott Hughes 671*e1fe3e4aSElliott Hughes if hasattr(hb, "repack_with_tag"): 672*e1fe3e4aSElliott Hughes return hb.repack_with_tag(str(tableTag), data, obj_list) 673*e1fe3e4aSElliott Hughes else: 674*e1fe3e4aSElliott Hughes return hb.repack(data, obj_list) 675*e1fe3e4aSElliott Hughes 676*e1fe3e4aSElliott Hughes def getAllData(self, remove_duplicate=True): 677*e1fe3e4aSElliott Hughes """Assemble all data, including all subtables.""" 678*e1fe3e4aSElliott Hughes if remove_duplicate: 679*e1fe3e4aSElliott Hughes internedTables = {} 680*e1fe3e4aSElliott Hughes self._doneWriting(internedTables) 681*e1fe3e4aSElliott Hughes tables = [] 682*e1fe3e4aSElliott Hughes extTables = [] 683*e1fe3e4aSElliott Hughes done = {} 684*e1fe3e4aSElliott Hughes self._gatherTables(tables, extTables, done) 685*e1fe3e4aSElliott Hughes tables.reverse() 686*e1fe3e4aSElliott Hughes extTables.reverse() 687*e1fe3e4aSElliott Hughes # Gather all data in two passes: the absolute positions of all 688*e1fe3e4aSElliott Hughes # subtable are needed before the actual data can be assembled. 689*e1fe3e4aSElliott Hughes pos = 0 690*e1fe3e4aSElliott Hughes for table in tables: 691*e1fe3e4aSElliott Hughes table.pos = pos 692*e1fe3e4aSElliott Hughes pos = pos + table.getDataLength() 693*e1fe3e4aSElliott Hughes 694*e1fe3e4aSElliott Hughes for table in extTables: 695*e1fe3e4aSElliott Hughes table.pos = pos 696*e1fe3e4aSElliott Hughes pos = pos + table.getDataLength() 697*e1fe3e4aSElliott Hughes 698*e1fe3e4aSElliott Hughes data = [] 699*e1fe3e4aSElliott Hughes for table in tables: 700*e1fe3e4aSElliott Hughes tableData = table.getData() 701*e1fe3e4aSElliott Hughes data.append(tableData) 702*e1fe3e4aSElliott Hughes 703*e1fe3e4aSElliott Hughes for table in extTables: 704*e1fe3e4aSElliott Hughes tableData = table.getData() 705*e1fe3e4aSElliott Hughes data.append(tableData) 706*e1fe3e4aSElliott Hughes 707*e1fe3e4aSElliott Hughes return bytesjoin(data) 708*e1fe3e4aSElliott Hughes 709*e1fe3e4aSElliott Hughes # interface for gathering data, as used by table.compile() 710*e1fe3e4aSElliott Hughes 711*e1fe3e4aSElliott Hughes def getSubWriter(self): 712*e1fe3e4aSElliott Hughes subwriter = self.__class__(self.localState, self.tableTag) 713*e1fe3e4aSElliott Hughes subwriter.parent = ( 714*e1fe3e4aSElliott Hughes self # because some subtables have idential values, we discard 715*e1fe3e4aSElliott Hughes ) 716*e1fe3e4aSElliott Hughes # the duplicates under the getAllData method. Hence some 717*e1fe3e4aSElliott Hughes # subtable writers can have more than one parent writer. 718*e1fe3e4aSElliott Hughes # But we just care about first one right now. 719*e1fe3e4aSElliott Hughes return subwriter 720*e1fe3e4aSElliott Hughes 721*e1fe3e4aSElliott Hughes def writeValue(self, typecode, value): 722*e1fe3e4aSElliott Hughes self.items.append(struct.pack(f">{typecode}", value)) 723*e1fe3e4aSElliott Hughes 724*e1fe3e4aSElliott Hughes def writeArray(self, typecode, values): 725*e1fe3e4aSElliott Hughes a = array.array(typecode, values) 726*e1fe3e4aSElliott Hughes if sys.byteorder != "big": 727*e1fe3e4aSElliott Hughes a.byteswap() 728*e1fe3e4aSElliott Hughes self.items.append(a.tobytes()) 729*e1fe3e4aSElliott Hughes 730*e1fe3e4aSElliott Hughes def writeInt8(self, value): 731*e1fe3e4aSElliott Hughes assert -128 <= value < 128, value 732*e1fe3e4aSElliott Hughes self.items.append(struct.pack(">b", value)) 733*e1fe3e4aSElliott Hughes 734*e1fe3e4aSElliott Hughes def writeInt8Array(self, values): 735*e1fe3e4aSElliott Hughes self.writeArray("b", values) 736*e1fe3e4aSElliott Hughes 737*e1fe3e4aSElliott Hughes def writeShort(self, value): 738*e1fe3e4aSElliott Hughes assert -32768 <= value < 32768, value 739*e1fe3e4aSElliott Hughes self.items.append(struct.pack(">h", value)) 740*e1fe3e4aSElliott Hughes 741*e1fe3e4aSElliott Hughes def writeShortArray(self, values): 742*e1fe3e4aSElliott Hughes self.writeArray("h", values) 743*e1fe3e4aSElliott Hughes 744*e1fe3e4aSElliott Hughes def writeLong(self, value): 745*e1fe3e4aSElliott Hughes self.items.append(struct.pack(">i", value)) 746*e1fe3e4aSElliott Hughes 747*e1fe3e4aSElliott Hughes def writeLongArray(self, values): 748*e1fe3e4aSElliott Hughes self.writeArray("i", values) 749*e1fe3e4aSElliott Hughes 750*e1fe3e4aSElliott Hughes def writeUInt8(self, value): 751*e1fe3e4aSElliott Hughes assert 0 <= value < 256, value 752*e1fe3e4aSElliott Hughes self.items.append(struct.pack(">B", value)) 753*e1fe3e4aSElliott Hughes 754*e1fe3e4aSElliott Hughes def writeUInt8Array(self, values): 755*e1fe3e4aSElliott Hughes self.writeArray("B", values) 756*e1fe3e4aSElliott Hughes 757*e1fe3e4aSElliott Hughes def writeUShort(self, value): 758*e1fe3e4aSElliott Hughes assert 0 <= value < 0x10000, value 759*e1fe3e4aSElliott Hughes self.items.append(struct.pack(">H", value)) 760*e1fe3e4aSElliott Hughes 761*e1fe3e4aSElliott Hughes def writeUShortArray(self, values): 762*e1fe3e4aSElliott Hughes self.writeArray("H", values) 763*e1fe3e4aSElliott Hughes 764*e1fe3e4aSElliott Hughes def writeULong(self, value): 765*e1fe3e4aSElliott Hughes self.items.append(struct.pack(">I", value)) 766*e1fe3e4aSElliott Hughes 767*e1fe3e4aSElliott Hughes def writeULongArray(self, values): 768*e1fe3e4aSElliott Hughes self.writeArray("I", values) 769*e1fe3e4aSElliott Hughes 770*e1fe3e4aSElliott Hughes def writeUInt24(self, value): 771*e1fe3e4aSElliott Hughes assert 0 <= value < 0x1000000, value 772*e1fe3e4aSElliott Hughes b = struct.pack(">L", value) 773*e1fe3e4aSElliott Hughes self.items.append(b[1:]) 774*e1fe3e4aSElliott Hughes 775*e1fe3e4aSElliott Hughes def writeUInt24Array(self, values): 776*e1fe3e4aSElliott Hughes for value in values: 777*e1fe3e4aSElliott Hughes self.writeUInt24(value) 778*e1fe3e4aSElliott Hughes 779*e1fe3e4aSElliott Hughes def writeTag(self, tag): 780*e1fe3e4aSElliott Hughes tag = Tag(tag).tobytes() 781*e1fe3e4aSElliott Hughes assert len(tag) == 4, tag 782*e1fe3e4aSElliott Hughes self.items.append(tag) 783*e1fe3e4aSElliott Hughes 784*e1fe3e4aSElliott Hughes def writeSubTable(self, subWriter, offsetSize): 785*e1fe3e4aSElliott Hughes self.items.append(OffsetToWriter(subWriter, offsetSize)) 786*e1fe3e4aSElliott Hughes 787*e1fe3e4aSElliott Hughes def writeCountReference(self, table, name, size=2, value=None): 788*e1fe3e4aSElliott Hughes ref = CountReference(table, name, size=size, value=value) 789*e1fe3e4aSElliott Hughes self.items.append(ref) 790*e1fe3e4aSElliott Hughes return ref 791*e1fe3e4aSElliott Hughes 792*e1fe3e4aSElliott Hughes def writeStruct(self, format, values): 793*e1fe3e4aSElliott Hughes data = struct.pack(*(format,) + values) 794*e1fe3e4aSElliott Hughes self.items.append(data) 795*e1fe3e4aSElliott Hughes 796*e1fe3e4aSElliott Hughes def writeData(self, data): 797*e1fe3e4aSElliott Hughes self.items.append(data) 798*e1fe3e4aSElliott Hughes 799*e1fe3e4aSElliott Hughes def getOverflowErrorRecord(self, item): 800*e1fe3e4aSElliott Hughes LookupListIndex = SubTableIndex = itemName = itemIndex = None 801*e1fe3e4aSElliott Hughes if self.name == "LookupList": 802*e1fe3e4aSElliott Hughes LookupListIndex = item.repeatIndex 803*e1fe3e4aSElliott Hughes elif self.name == "Lookup": 804*e1fe3e4aSElliott Hughes LookupListIndex = self.repeatIndex 805*e1fe3e4aSElliott Hughes SubTableIndex = item.repeatIndex 806*e1fe3e4aSElliott Hughes else: 807*e1fe3e4aSElliott Hughes itemName = getattr(item, "name", "<none>") 808*e1fe3e4aSElliott Hughes if hasattr(item, "repeatIndex"): 809*e1fe3e4aSElliott Hughes itemIndex = item.repeatIndex 810*e1fe3e4aSElliott Hughes if self.name == "SubTable": 811*e1fe3e4aSElliott Hughes LookupListIndex = self.parent.repeatIndex 812*e1fe3e4aSElliott Hughes SubTableIndex = self.repeatIndex 813*e1fe3e4aSElliott Hughes elif self.name == "ExtSubTable": 814*e1fe3e4aSElliott Hughes LookupListIndex = self.parent.parent.repeatIndex 815*e1fe3e4aSElliott Hughes SubTableIndex = self.parent.repeatIndex 816*e1fe3e4aSElliott Hughes else: # who knows how far below the SubTable level we are! Climb back up to the nearest subtable. 817*e1fe3e4aSElliott Hughes itemName = ".".join([self.name, itemName]) 818*e1fe3e4aSElliott Hughes p1 = self.parent 819*e1fe3e4aSElliott Hughes while p1 and p1.name not in ["ExtSubTable", "SubTable"]: 820*e1fe3e4aSElliott Hughes itemName = ".".join([p1.name, itemName]) 821*e1fe3e4aSElliott Hughes p1 = p1.parent 822*e1fe3e4aSElliott Hughes if p1: 823*e1fe3e4aSElliott Hughes if p1.name == "ExtSubTable": 824*e1fe3e4aSElliott Hughes LookupListIndex = p1.parent.parent.repeatIndex 825*e1fe3e4aSElliott Hughes SubTableIndex = p1.parent.repeatIndex 826*e1fe3e4aSElliott Hughes else: 827*e1fe3e4aSElliott Hughes LookupListIndex = p1.parent.repeatIndex 828*e1fe3e4aSElliott Hughes SubTableIndex = p1.repeatIndex 829*e1fe3e4aSElliott Hughes 830*e1fe3e4aSElliott Hughes return OverflowErrorRecord( 831*e1fe3e4aSElliott Hughes (self.tableTag, LookupListIndex, SubTableIndex, itemName, itemIndex) 832*e1fe3e4aSElliott Hughes ) 833*e1fe3e4aSElliott Hughes 834*e1fe3e4aSElliott Hughes 835*e1fe3e4aSElliott Hughesclass CountReference(object): 836*e1fe3e4aSElliott Hughes """A reference to a Count value, not a count of references.""" 837*e1fe3e4aSElliott Hughes 838*e1fe3e4aSElliott Hughes def __init__(self, table, name, size=None, value=None): 839*e1fe3e4aSElliott Hughes self.table = table 840*e1fe3e4aSElliott Hughes self.name = name 841*e1fe3e4aSElliott Hughes self.size = size 842*e1fe3e4aSElliott Hughes if value is not None: 843*e1fe3e4aSElliott Hughes self.setValue(value) 844*e1fe3e4aSElliott Hughes 845*e1fe3e4aSElliott Hughes def setValue(self, value): 846*e1fe3e4aSElliott Hughes table = self.table 847*e1fe3e4aSElliott Hughes name = self.name 848*e1fe3e4aSElliott Hughes if table[name] is None: 849*e1fe3e4aSElliott Hughes table[name] = value 850*e1fe3e4aSElliott Hughes else: 851*e1fe3e4aSElliott Hughes assert table[name] == value, (name, table[name], value) 852*e1fe3e4aSElliott Hughes 853*e1fe3e4aSElliott Hughes def getValue(self): 854*e1fe3e4aSElliott Hughes return self.table[self.name] 855*e1fe3e4aSElliott Hughes 856*e1fe3e4aSElliott Hughes def getCountData(self): 857*e1fe3e4aSElliott Hughes v = self.table[self.name] 858*e1fe3e4aSElliott Hughes if v is None: 859*e1fe3e4aSElliott Hughes v = 0 860*e1fe3e4aSElliott Hughes return {1: packUInt8, 2: packUShort, 4: packULong}[self.size](v) 861*e1fe3e4aSElliott Hughes 862*e1fe3e4aSElliott Hughes 863*e1fe3e4aSElliott Hughesdef packUInt8(value): 864*e1fe3e4aSElliott Hughes return struct.pack(">B", value) 865*e1fe3e4aSElliott Hughes 866*e1fe3e4aSElliott Hughes 867*e1fe3e4aSElliott Hughesdef packUShort(value): 868*e1fe3e4aSElliott Hughes return struct.pack(">H", value) 869*e1fe3e4aSElliott Hughes 870*e1fe3e4aSElliott Hughes 871*e1fe3e4aSElliott Hughesdef packULong(value): 872*e1fe3e4aSElliott Hughes assert 0 <= value < 0x100000000, value 873*e1fe3e4aSElliott Hughes return struct.pack(">I", value) 874*e1fe3e4aSElliott Hughes 875*e1fe3e4aSElliott Hughes 876*e1fe3e4aSElliott Hughesdef packUInt24(value): 877*e1fe3e4aSElliott Hughes assert 0 <= value < 0x1000000, value 878*e1fe3e4aSElliott Hughes return struct.pack(">I", value)[1:] 879*e1fe3e4aSElliott Hughes 880*e1fe3e4aSElliott Hughes 881*e1fe3e4aSElliott Hughesclass BaseTable(object): 882*e1fe3e4aSElliott Hughes """Generic base class for all OpenType (sub)tables.""" 883*e1fe3e4aSElliott Hughes 884*e1fe3e4aSElliott Hughes def __getattr__(self, attr): 885*e1fe3e4aSElliott Hughes reader = self.__dict__.get("reader") 886*e1fe3e4aSElliott Hughes if reader: 887*e1fe3e4aSElliott Hughes del self.reader 888*e1fe3e4aSElliott Hughes font = self.font 889*e1fe3e4aSElliott Hughes del self.font 890*e1fe3e4aSElliott Hughes self.decompile(reader, font) 891*e1fe3e4aSElliott Hughes return getattr(self, attr) 892*e1fe3e4aSElliott Hughes 893*e1fe3e4aSElliott Hughes raise AttributeError(attr) 894*e1fe3e4aSElliott Hughes 895*e1fe3e4aSElliott Hughes def ensureDecompiled(self, recurse=False): 896*e1fe3e4aSElliott Hughes reader = self.__dict__.get("reader") 897*e1fe3e4aSElliott Hughes if reader: 898*e1fe3e4aSElliott Hughes del self.reader 899*e1fe3e4aSElliott Hughes font = self.font 900*e1fe3e4aSElliott Hughes del self.font 901*e1fe3e4aSElliott Hughes self.decompile(reader, font) 902*e1fe3e4aSElliott Hughes if recurse: 903*e1fe3e4aSElliott Hughes for subtable in self.iterSubTables(): 904*e1fe3e4aSElliott Hughes subtable.value.ensureDecompiled(recurse) 905*e1fe3e4aSElliott Hughes 906*e1fe3e4aSElliott Hughes def __getstate__(self): 907*e1fe3e4aSElliott Hughes # before copying/pickling 'lazy' objects, make a shallow copy of OTTableReader 908*e1fe3e4aSElliott Hughes # https://github.com/fonttools/fonttools/issues/2965 909*e1fe3e4aSElliott Hughes if "reader" in self.__dict__: 910*e1fe3e4aSElliott Hughes state = self.__dict__.copy() 911*e1fe3e4aSElliott Hughes state["reader"] = self.__dict__["reader"].copy() 912*e1fe3e4aSElliott Hughes return state 913*e1fe3e4aSElliott Hughes return self.__dict__ 914*e1fe3e4aSElliott Hughes 915*e1fe3e4aSElliott Hughes @classmethod 916*e1fe3e4aSElliott Hughes def getRecordSize(cls, reader): 917*e1fe3e4aSElliott Hughes totalSize = 0 918*e1fe3e4aSElliott Hughes for conv in cls.converters: 919*e1fe3e4aSElliott Hughes size = conv.getRecordSize(reader) 920*e1fe3e4aSElliott Hughes if size is NotImplemented: 921*e1fe3e4aSElliott Hughes return NotImplemented 922*e1fe3e4aSElliott Hughes countValue = 1 923*e1fe3e4aSElliott Hughes if conv.repeat: 924*e1fe3e4aSElliott Hughes if conv.repeat in reader: 925*e1fe3e4aSElliott Hughes countValue = reader[conv.repeat] + conv.aux 926*e1fe3e4aSElliott Hughes else: 927*e1fe3e4aSElliott Hughes return NotImplemented 928*e1fe3e4aSElliott Hughes totalSize += size * countValue 929*e1fe3e4aSElliott Hughes return totalSize 930*e1fe3e4aSElliott Hughes 931*e1fe3e4aSElliott Hughes def getConverters(self): 932*e1fe3e4aSElliott Hughes return self.converters 933*e1fe3e4aSElliott Hughes 934*e1fe3e4aSElliott Hughes def getConverterByName(self, name): 935*e1fe3e4aSElliott Hughes return self.convertersByName[name] 936*e1fe3e4aSElliott Hughes 937*e1fe3e4aSElliott Hughes def populateDefaults(self, propagator=None): 938*e1fe3e4aSElliott Hughes for conv in self.getConverters(): 939*e1fe3e4aSElliott Hughes if conv.repeat: 940*e1fe3e4aSElliott Hughes if not hasattr(self, conv.name): 941*e1fe3e4aSElliott Hughes setattr(self, conv.name, []) 942*e1fe3e4aSElliott Hughes countValue = len(getattr(self, conv.name)) - conv.aux 943*e1fe3e4aSElliott Hughes try: 944*e1fe3e4aSElliott Hughes count_conv = self.getConverterByName(conv.repeat) 945*e1fe3e4aSElliott Hughes setattr(self, conv.repeat, countValue) 946*e1fe3e4aSElliott Hughes except KeyError: 947*e1fe3e4aSElliott Hughes # conv.repeat is a propagated count 948*e1fe3e4aSElliott Hughes if propagator and conv.repeat in propagator: 949*e1fe3e4aSElliott Hughes propagator[conv.repeat].setValue(countValue) 950*e1fe3e4aSElliott Hughes else: 951*e1fe3e4aSElliott Hughes if conv.aux and not eval(conv.aux, None, self.__dict__): 952*e1fe3e4aSElliott Hughes continue 953*e1fe3e4aSElliott Hughes if hasattr(self, conv.name): 954*e1fe3e4aSElliott Hughes continue # Warn if it should NOT be present?! 955*e1fe3e4aSElliott Hughes if hasattr(conv, "writeNullOffset"): 956*e1fe3e4aSElliott Hughes setattr(self, conv.name, None) # Warn? 957*e1fe3e4aSElliott Hughes # elif not conv.isCount: 958*e1fe3e4aSElliott Hughes # # Warn? 959*e1fe3e4aSElliott Hughes # pass 960*e1fe3e4aSElliott Hughes if hasattr(conv, "DEFAULT"): 961*e1fe3e4aSElliott Hughes # OptionalValue converters (e.g. VarIndex) 962*e1fe3e4aSElliott Hughes setattr(self, conv.name, conv.DEFAULT) 963*e1fe3e4aSElliott Hughes 964*e1fe3e4aSElliott Hughes def decompile(self, reader, font): 965*e1fe3e4aSElliott Hughes self.readFormat(reader) 966*e1fe3e4aSElliott Hughes table = {} 967*e1fe3e4aSElliott Hughes self.__rawTable = table # for debugging 968*e1fe3e4aSElliott Hughes for conv in self.getConverters(): 969*e1fe3e4aSElliott Hughes if conv.name == "SubTable": 970*e1fe3e4aSElliott Hughes conv = conv.getConverter(reader.tableTag, table["LookupType"]) 971*e1fe3e4aSElliott Hughes if conv.name == "ExtSubTable": 972*e1fe3e4aSElliott Hughes conv = conv.getConverter(reader.tableTag, table["ExtensionLookupType"]) 973*e1fe3e4aSElliott Hughes if conv.name == "FeatureParams": 974*e1fe3e4aSElliott Hughes conv = conv.getConverter(reader["FeatureTag"]) 975*e1fe3e4aSElliott Hughes if conv.name == "SubStruct": 976*e1fe3e4aSElliott Hughes conv = conv.getConverter(reader.tableTag, table["MorphType"]) 977*e1fe3e4aSElliott Hughes try: 978*e1fe3e4aSElliott Hughes if conv.repeat: 979*e1fe3e4aSElliott Hughes if isinstance(conv.repeat, int): 980*e1fe3e4aSElliott Hughes countValue = conv.repeat 981*e1fe3e4aSElliott Hughes elif conv.repeat in table: 982*e1fe3e4aSElliott Hughes countValue = table[conv.repeat] 983*e1fe3e4aSElliott Hughes else: 984*e1fe3e4aSElliott Hughes # conv.repeat is a propagated count 985*e1fe3e4aSElliott Hughes countValue = reader[conv.repeat] 986*e1fe3e4aSElliott Hughes countValue += conv.aux 987*e1fe3e4aSElliott Hughes table[conv.name] = conv.readArray(reader, font, table, countValue) 988*e1fe3e4aSElliott Hughes else: 989*e1fe3e4aSElliott Hughes if conv.aux and not eval(conv.aux, None, table): 990*e1fe3e4aSElliott Hughes continue 991*e1fe3e4aSElliott Hughes table[conv.name] = conv.read(reader, font, table) 992*e1fe3e4aSElliott Hughes if conv.isPropagated: 993*e1fe3e4aSElliott Hughes reader[conv.name] = table[conv.name] 994*e1fe3e4aSElliott Hughes except Exception as e: 995*e1fe3e4aSElliott Hughes name = conv.name 996*e1fe3e4aSElliott Hughes e.args = e.args + (name,) 997*e1fe3e4aSElliott Hughes raise 998*e1fe3e4aSElliott Hughes 999*e1fe3e4aSElliott Hughes if hasattr(self, "postRead"): 1000*e1fe3e4aSElliott Hughes self.postRead(table, font) 1001*e1fe3e4aSElliott Hughes else: 1002*e1fe3e4aSElliott Hughes self.__dict__.update(table) 1003*e1fe3e4aSElliott Hughes 1004*e1fe3e4aSElliott Hughes del self.__rawTable # succeeded, get rid of debugging info 1005*e1fe3e4aSElliott Hughes 1006*e1fe3e4aSElliott Hughes def compile(self, writer, font): 1007*e1fe3e4aSElliott Hughes self.ensureDecompiled() 1008*e1fe3e4aSElliott Hughes # TODO Following hack to be removed by rewriting how FormatSwitching tables 1009*e1fe3e4aSElliott Hughes # are handled. 1010*e1fe3e4aSElliott Hughes # https://github.com/fonttools/fonttools/pull/2238#issuecomment-805192631 1011*e1fe3e4aSElliott Hughes if hasattr(self, "preWrite"): 1012*e1fe3e4aSElliott Hughes deleteFormat = not hasattr(self, "Format") 1013*e1fe3e4aSElliott Hughes table = self.preWrite(font) 1014*e1fe3e4aSElliott Hughes deleteFormat = deleteFormat and hasattr(self, "Format") 1015*e1fe3e4aSElliott Hughes else: 1016*e1fe3e4aSElliott Hughes deleteFormat = False 1017*e1fe3e4aSElliott Hughes table = self.__dict__.copy() 1018*e1fe3e4aSElliott Hughes 1019*e1fe3e4aSElliott Hughes # some count references may have been initialized in a custom preWrite; we set 1020*e1fe3e4aSElliott Hughes # these in the writer's state beforehand (instead of sequentially) so they will 1021*e1fe3e4aSElliott Hughes # be propagated to all nested subtables even if the count appears in the current 1022*e1fe3e4aSElliott Hughes # table only *after* the offset to the subtable that it is counting. 1023*e1fe3e4aSElliott Hughes for conv in self.getConverters(): 1024*e1fe3e4aSElliott Hughes if conv.isCount and conv.isPropagated: 1025*e1fe3e4aSElliott Hughes value = table.get(conv.name) 1026*e1fe3e4aSElliott Hughes if isinstance(value, CountReference): 1027*e1fe3e4aSElliott Hughes writer[conv.name] = value 1028*e1fe3e4aSElliott Hughes 1029*e1fe3e4aSElliott Hughes if hasattr(self, "sortCoverageLast"): 1030*e1fe3e4aSElliott Hughes writer.sortCoverageLast = 1 1031*e1fe3e4aSElliott Hughes 1032*e1fe3e4aSElliott Hughes if hasattr(self, "DontShare"): 1033*e1fe3e4aSElliott Hughes writer.DontShare = True 1034*e1fe3e4aSElliott Hughes 1035*e1fe3e4aSElliott Hughes if hasattr(self.__class__, "LookupType"): 1036*e1fe3e4aSElliott Hughes writer["LookupType"].setValue(self.__class__.LookupType) 1037*e1fe3e4aSElliott Hughes 1038*e1fe3e4aSElliott Hughes self.writeFormat(writer) 1039*e1fe3e4aSElliott Hughes for conv in self.getConverters(): 1040*e1fe3e4aSElliott Hughes value = table.get( 1041*e1fe3e4aSElliott Hughes conv.name 1042*e1fe3e4aSElliott Hughes ) # TODO Handle defaults instead of defaulting to None! 1043*e1fe3e4aSElliott Hughes if conv.repeat: 1044*e1fe3e4aSElliott Hughes if value is None: 1045*e1fe3e4aSElliott Hughes value = [] 1046*e1fe3e4aSElliott Hughes countValue = len(value) - conv.aux 1047*e1fe3e4aSElliott Hughes if isinstance(conv.repeat, int): 1048*e1fe3e4aSElliott Hughes assert len(value) == conv.repeat, "expected %d values, got %d" % ( 1049*e1fe3e4aSElliott Hughes conv.repeat, 1050*e1fe3e4aSElliott Hughes len(value), 1051*e1fe3e4aSElliott Hughes ) 1052*e1fe3e4aSElliott Hughes elif conv.repeat in table: 1053*e1fe3e4aSElliott Hughes CountReference(table, conv.repeat, value=countValue) 1054*e1fe3e4aSElliott Hughes else: 1055*e1fe3e4aSElliott Hughes # conv.repeat is a propagated count 1056*e1fe3e4aSElliott Hughes writer[conv.repeat].setValue(countValue) 1057*e1fe3e4aSElliott Hughes try: 1058*e1fe3e4aSElliott Hughes conv.writeArray(writer, font, table, value) 1059*e1fe3e4aSElliott Hughes except Exception as e: 1060*e1fe3e4aSElliott Hughes e.args = e.args + (conv.name + "[]",) 1061*e1fe3e4aSElliott Hughes raise 1062*e1fe3e4aSElliott Hughes elif conv.isCount: 1063*e1fe3e4aSElliott Hughes # Special-case Count values. 1064*e1fe3e4aSElliott Hughes # Assumption: a Count field will *always* precede 1065*e1fe3e4aSElliott Hughes # the actual array(s). 1066*e1fe3e4aSElliott Hughes # We need a default value, as it may be set later by a nested 1067*e1fe3e4aSElliott Hughes # table. We will later store it here. 1068*e1fe3e4aSElliott Hughes # We add a reference: by the time the data is assembled 1069*e1fe3e4aSElliott Hughes # the Count value will be filled in. 1070*e1fe3e4aSElliott Hughes # We ignore the current count value since it will be recomputed, 1071*e1fe3e4aSElliott Hughes # unless it's a CountReference that was already initialized in a custom preWrite. 1072*e1fe3e4aSElliott Hughes if isinstance(value, CountReference): 1073*e1fe3e4aSElliott Hughes ref = value 1074*e1fe3e4aSElliott Hughes ref.size = conv.staticSize 1075*e1fe3e4aSElliott Hughes writer.writeData(ref) 1076*e1fe3e4aSElliott Hughes table[conv.name] = ref.getValue() 1077*e1fe3e4aSElliott Hughes else: 1078*e1fe3e4aSElliott Hughes ref = writer.writeCountReference(table, conv.name, conv.staticSize) 1079*e1fe3e4aSElliott Hughes table[conv.name] = None 1080*e1fe3e4aSElliott Hughes if conv.isPropagated: 1081*e1fe3e4aSElliott Hughes writer[conv.name] = ref 1082*e1fe3e4aSElliott Hughes elif conv.isLookupType: 1083*e1fe3e4aSElliott Hughes # We make sure that subtables have the same lookup type, 1084*e1fe3e4aSElliott Hughes # and that the type is the same as the one set on the 1085*e1fe3e4aSElliott Hughes # Lookup object, if any is set. 1086*e1fe3e4aSElliott Hughes if conv.name not in table: 1087*e1fe3e4aSElliott Hughes table[conv.name] = None 1088*e1fe3e4aSElliott Hughes ref = writer.writeCountReference( 1089*e1fe3e4aSElliott Hughes table, conv.name, conv.staticSize, table[conv.name] 1090*e1fe3e4aSElliott Hughes ) 1091*e1fe3e4aSElliott Hughes writer["LookupType"] = ref 1092*e1fe3e4aSElliott Hughes else: 1093*e1fe3e4aSElliott Hughes if conv.aux and not eval(conv.aux, None, table): 1094*e1fe3e4aSElliott Hughes continue 1095*e1fe3e4aSElliott Hughes try: 1096*e1fe3e4aSElliott Hughes conv.write(writer, font, table, value) 1097*e1fe3e4aSElliott Hughes except Exception as e: 1098*e1fe3e4aSElliott Hughes name = value.__class__.__name__ if value is not None else conv.name 1099*e1fe3e4aSElliott Hughes e.args = e.args + (name,) 1100*e1fe3e4aSElliott Hughes raise 1101*e1fe3e4aSElliott Hughes if conv.isPropagated: 1102*e1fe3e4aSElliott Hughes writer[conv.name] = value 1103*e1fe3e4aSElliott Hughes 1104*e1fe3e4aSElliott Hughes if deleteFormat: 1105*e1fe3e4aSElliott Hughes del self.Format 1106*e1fe3e4aSElliott Hughes 1107*e1fe3e4aSElliott Hughes def readFormat(self, reader): 1108*e1fe3e4aSElliott Hughes pass 1109*e1fe3e4aSElliott Hughes 1110*e1fe3e4aSElliott Hughes def writeFormat(self, writer): 1111*e1fe3e4aSElliott Hughes pass 1112*e1fe3e4aSElliott Hughes 1113*e1fe3e4aSElliott Hughes def toXML(self, xmlWriter, font, attrs=None, name=None): 1114*e1fe3e4aSElliott Hughes tableName = name if name else self.__class__.__name__ 1115*e1fe3e4aSElliott Hughes if attrs is None: 1116*e1fe3e4aSElliott Hughes attrs = [] 1117*e1fe3e4aSElliott Hughes if hasattr(self, "Format"): 1118*e1fe3e4aSElliott Hughes attrs = attrs + [("Format", self.Format)] 1119*e1fe3e4aSElliott Hughes xmlWriter.begintag(tableName, attrs) 1120*e1fe3e4aSElliott Hughes xmlWriter.newline() 1121*e1fe3e4aSElliott Hughes self.toXML2(xmlWriter, font) 1122*e1fe3e4aSElliott Hughes xmlWriter.endtag(tableName) 1123*e1fe3e4aSElliott Hughes xmlWriter.newline() 1124*e1fe3e4aSElliott Hughes 1125*e1fe3e4aSElliott Hughes def toXML2(self, xmlWriter, font): 1126*e1fe3e4aSElliott Hughes # Simpler variant of toXML, *only* for the top level tables (like GPOS, GSUB). 1127*e1fe3e4aSElliott Hughes # This is because in TTX our parent writes our main tag, and in otBase.py we 1128*e1fe3e4aSElliott Hughes # do it ourselves. I think I'm getting schizophrenic... 1129*e1fe3e4aSElliott Hughes for conv in self.getConverters(): 1130*e1fe3e4aSElliott Hughes if conv.repeat: 1131*e1fe3e4aSElliott Hughes value = getattr(self, conv.name, []) 1132*e1fe3e4aSElliott Hughes for i in range(len(value)): 1133*e1fe3e4aSElliott Hughes item = value[i] 1134*e1fe3e4aSElliott Hughes conv.xmlWrite(xmlWriter, font, item, conv.name, [("index", i)]) 1135*e1fe3e4aSElliott Hughes else: 1136*e1fe3e4aSElliott Hughes if conv.aux and not eval(conv.aux, None, vars(self)): 1137*e1fe3e4aSElliott Hughes continue 1138*e1fe3e4aSElliott Hughes value = getattr( 1139*e1fe3e4aSElliott Hughes self, conv.name, None 1140*e1fe3e4aSElliott Hughes ) # TODO Handle defaults instead of defaulting to None! 1141*e1fe3e4aSElliott Hughes conv.xmlWrite(xmlWriter, font, value, conv.name, []) 1142*e1fe3e4aSElliott Hughes 1143*e1fe3e4aSElliott Hughes def fromXML(self, name, attrs, content, font): 1144*e1fe3e4aSElliott Hughes try: 1145*e1fe3e4aSElliott Hughes conv = self.getConverterByName(name) 1146*e1fe3e4aSElliott Hughes except KeyError: 1147*e1fe3e4aSElliott Hughes raise # XXX on KeyError, raise nice error 1148*e1fe3e4aSElliott Hughes value = conv.xmlRead(attrs, content, font) 1149*e1fe3e4aSElliott Hughes if conv.repeat: 1150*e1fe3e4aSElliott Hughes seq = getattr(self, conv.name, None) 1151*e1fe3e4aSElliott Hughes if seq is None: 1152*e1fe3e4aSElliott Hughes seq = [] 1153*e1fe3e4aSElliott Hughes setattr(self, conv.name, seq) 1154*e1fe3e4aSElliott Hughes seq.append(value) 1155*e1fe3e4aSElliott Hughes else: 1156*e1fe3e4aSElliott Hughes setattr(self, conv.name, value) 1157*e1fe3e4aSElliott Hughes 1158*e1fe3e4aSElliott Hughes def __ne__(self, other): 1159*e1fe3e4aSElliott Hughes result = self.__eq__(other) 1160*e1fe3e4aSElliott Hughes return result if result is NotImplemented else not result 1161*e1fe3e4aSElliott Hughes 1162*e1fe3e4aSElliott Hughes def __eq__(self, other): 1163*e1fe3e4aSElliott Hughes if type(self) != type(other): 1164*e1fe3e4aSElliott Hughes return NotImplemented 1165*e1fe3e4aSElliott Hughes 1166*e1fe3e4aSElliott Hughes self.ensureDecompiled() 1167*e1fe3e4aSElliott Hughes other.ensureDecompiled() 1168*e1fe3e4aSElliott Hughes 1169*e1fe3e4aSElliott Hughes return self.__dict__ == other.__dict__ 1170*e1fe3e4aSElliott Hughes 1171*e1fe3e4aSElliott Hughes class SubTableEntry(NamedTuple): 1172*e1fe3e4aSElliott Hughes """See BaseTable.iterSubTables()""" 1173*e1fe3e4aSElliott Hughes 1174*e1fe3e4aSElliott Hughes name: str 1175*e1fe3e4aSElliott Hughes value: "BaseTable" 1176*e1fe3e4aSElliott Hughes index: Optional[int] = None # index into given array, None for single values 1177*e1fe3e4aSElliott Hughes 1178*e1fe3e4aSElliott Hughes def iterSubTables(self) -> Iterator[SubTableEntry]: 1179*e1fe3e4aSElliott Hughes """Yield (name, value, index) namedtuples for all subtables of current table. 1180*e1fe3e4aSElliott Hughes 1181*e1fe3e4aSElliott Hughes A sub-table is an instance of BaseTable (or subclass thereof) that is a child 1182*e1fe3e4aSElliott Hughes of self, the current parent table. 1183*e1fe3e4aSElliott Hughes The tuples also contain the attribute name (str) of the of parent table to get 1184*e1fe3e4aSElliott Hughes a subtable, and optionally, for lists of subtables (i.e. attributes associated 1185*e1fe3e4aSElliott Hughes with a converter that has a 'repeat'), an index into the list containing the 1186*e1fe3e4aSElliott Hughes given subtable value. 1187*e1fe3e4aSElliott Hughes This method can be useful to traverse trees of otTables. 1188*e1fe3e4aSElliott Hughes """ 1189*e1fe3e4aSElliott Hughes for conv in self.getConverters(): 1190*e1fe3e4aSElliott Hughes name = conv.name 1191*e1fe3e4aSElliott Hughes value = getattr(self, name, None) 1192*e1fe3e4aSElliott Hughes if value is None: 1193*e1fe3e4aSElliott Hughes continue 1194*e1fe3e4aSElliott Hughes if isinstance(value, BaseTable): 1195*e1fe3e4aSElliott Hughes yield self.SubTableEntry(name, value) 1196*e1fe3e4aSElliott Hughes elif isinstance(value, list): 1197*e1fe3e4aSElliott Hughes yield from ( 1198*e1fe3e4aSElliott Hughes self.SubTableEntry(name, v, index=i) 1199*e1fe3e4aSElliott Hughes for i, v in enumerate(value) 1200*e1fe3e4aSElliott Hughes if isinstance(v, BaseTable) 1201*e1fe3e4aSElliott Hughes ) 1202*e1fe3e4aSElliott Hughes 1203*e1fe3e4aSElliott Hughes # instance (not @class)method for consistency with FormatSwitchingBaseTable 1204*e1fe3e4aSElliott Hughes def getVariableAttrs(self): 1205*e1fe3e4aSElliott Hughes return getVariableAttrs(self.__class__) 1206*e1fe3e4aSElliott Hughes 1207*e1fe3e4aSElliott Hughes 1208*e1fe3e4aSElliott Hughesclass FormatSwitchingBaseTable(BaseTable): 1209*e1fe3e4aSElliott Hughes """Minor specialization of BaseTable, for tables that have multiple 1210*e1fe3e4aSElliott Hughes formats, eg. CoverageFormat1 vs. CoverageFormat2.""" 1211*e1fe3e4aSElliott Hughes 1212*e1fe3e4aSElliott Hughes @classmethod 1213*e1fe3e4aSElliott Hughes def getRecordSize(cls, reader): 1214*e1fe3e4aSElliott Hughes return NotImplemented 1215*e1fe3e4aSElliott Hughes 1216*e1fe3e4aSElliott Hughes def getConverters(self): 1217*e1fe3e4aSElliott Hughes try: 1218*e1fe3e4aSElliott Hughes fmt = self.Format 1219*e1fe3e4aSElliott Hughes except AttributeError: 1220*e1fe3e4aSElliott Hughes # some FormatSwitchingBaseTables (e.g. Coverage) no longer have 'Format' 1221*e1fe3e4aSElliott Hughes # attribute after fully decompiled, only gain one in preWrite before being 1222*e1fe3e4aSElliott Hughes # recompiled. In the decompiled state, these hand-coded classes defined in 1223*e1fe3e4aSElliott Hughes # otTables.py lose their format-specific nature and gain more high-level 1224*e1fe3e4aSElliott Hughes # attributes that are not tied to converters. 1225*e1fe3e4aSElliott Hughes return [] 1226*e1fe3e4aSElliott Hughes return self.converters.get(self.Format, []) 1227*e1fe3e4aSElliott Hughes 1228*e1fe3e4aSElliott Hughes def getConverterByName(self, name): 1229*e1fe3e4aSElliott Hughes return self.convertersByName[self.Format][name] 1230*e1fe3e4aSElliott Hughes 1231*e1fe3e4aSElliott Hughes def readFormat(self, reader): 1232*e1fe3e4aSElliott Hughes self.Format = reader.readUShort() 1233*e1fe3e4aSElliott Hughes 1234*e1fe3e4aSElliott Hughes def writeFormat(self, writer): 1235*e1fe3e4aSElliott Hughes writer.writeUShort(self.Format) 1236*e1fe3e4aSElliott Hughes 1237*e1fe3e4aSElliott Hughes def toXML(self, xmlWriter, font, attrs=None, name=None): 1238*e1fe3e4aSElliott Hughes BaseTable.toXML(self, xmlWriter, font, attrs, name) 1239*e1fe3e4aSElliott Hughes 1240*e1fe3e4aSElliott Hughes def getVariableAttrs(self): 1241*e1fe3e4aSElliott Hughes return getVariableAttrs(self.__class__, self.Format) 1242*e1fe3e4aSElliott Hughes 1243*e1fe3e4aSElliott Hughes 1244*e1fe3e4aSElliott Hughesclass UInt8FormatSwitchingBaseTable(FormatSwitchingBaseTable): 1245*e1fe3e4aSElliott Hughes def readFormat(self, reader): 1246*e1fe3e4aSElliott Hughes self.Format = reader.readUInt8() 1247*e1fe3e4aSElliott Hughes 1248*e1fe3e4aSElliott Hughes def writeFormat(self, writer): 1249*e1fe3e4aSElliott Hughes writer.writeUInt8(self.Format) 1250*e1fe3e4aSElliott Hughes 1251*e1fe3e4aSElliott Hughes 1252*e1fe3e4aSElliott HughesformatSwitchingBaseTables = { 1253*e1fe3e4aSElliott Hughes "uint16": FormatSwitchingBaseTable, 1254*e1fe3e4aSElliott Hughes "uint8": UInt8FormatSwitchingBaseTable, 1255*e1fe3e4aSElliott Hughes} 1256*e1fe3e4aSElliott Hughes 1257*e1fe3e4aSElliott Hughes 1258*e1fe3e4aSElliott Hughesdef getFormatSwitchingBaseTableClass(formatType): 1259*e1fe3e4aSElliott Hughes try: 1260*e1fe3e4aSElliott Hughes return formatSwitchingBaseTables[formatType] 1261*e1fe3e4aSElliott Hughes except KeyError: 1262*e1fe3e4aSElliott Hughes raise TypeError(f"Unsupported format type: {formatType!r}") 1263*e1fe3e4aSElliott Hughes 1264*e1fe3e4aSElliott Hughes 1265*e1fe3e4aSElliott Hughes# memoize since these are parsed from otData.py, thus stay constant 1266*e1fe3e4aSElliott Hughes@lru_cache() 1267*e1fe3e4aSElliott Hughesdef getVariableAttrs(cls: BaseTable, fmt: Optional[int] = None) -> Tuple[str]: 1268*e1fe3e4aSElliott Hughes """Return sequence of variable table field names (can be empty). 1269*e1fe3e4aSElliott Hughes 1270*e1fe3e4aSElliott Hughes Attributes are deemed "variable" when their otData.py's description contain 1271*e1fe3e4aSElliott Hughes 'VarIndexBase + {offset}', e.g. COLRv1 PaintVar* tables. 1272*e1fe3e4aSElliott Hughes """ 1273*e1fe3e4aSElliott Hughes if not issubclass(cls, BaseTable): 1274*e1fe3e4aSElliott Hughes raise TypeError(cls) 1275*e1fe3e4aSElliott Hughes if issubclass(cls, FormatSwitchingBaseTable): 1276*e1fe3e4aSElliott Hughes if fmt is None: 1277*e1fe3e4aSElliott Hughes raise TypeError(f"'fmt' is required for format-switching {cls.__name__}") 1278*e1fe3e4aSElliott Hughes converters = cls.convertersByName[fmt] 1279*e1fe3e4aSElliott Hughes else: 1280*e1fe3e4aSElliott Hughes converters = cls.convertersByName 1281*e1fe3e4aSElliott Hughes # assume if no 'VarIndexBase' field is present, table has no variable fields 1282*e1fe3e4aSElliott Hughes if "VarIndexBase" not in converters: 1283*e1fe3e4aSElliott Hughes return () 1284*e1fe3e4aSElliott Hughes varAttrs = {} 1285*e1fe3e4aSElliott Hughes for name, conv in converters.items(): 1286*e1fe3e4aSElliott Hughes offset = conv.getVarIndexOffset() 1287*e1fe3e4aSElliott Hughes if offset is not None: 1288*e1fe3e4aSElliott Hughes varAttrs[name] = offset 1289*e1fe3e4aSElliott Hughes return tuple(sorted(varAttrs, key=varAttrs.__getitem__)) 1290*e1fe3e4aSElliott Hughes 1291*e1fe3e4aSElliott Hughes 1292*e1fe3e4aSElliott Hughes# 1293*e1fe3e4aSElliott Hughes# Support for ValueRecords 1294*e1fe3e4aSElliott Hughes# 1295*e1fe3e4aSElliott Hughes# This data type is so different from all other OpenType data types that 1296*e1fe3e4aSElliott Hughes# it requires quite a bit of code for itself. It even has special support 1297*e1fe3e4aSElliott Hughes# in OTTableReader and OTTableWriter... 1298*e1fe3e4aSElliott Hughes# 1299*e1fe3e4aSElliott Hughes 1300*e1fe3e4aSElliott HughesvalueRecordFormat = [ 1301*e1fe3e4aSElliott Hughes # Mask Name isDevice signed 1302*e1fe3e4aSElliott Hughes (0x0001, "XPlacement", 0, 1), 1303*e1fe3e4aSElliott Hughes (0x0002, "YPlacement", 0, 1), 1304*e1fe3e4aSElliott Hughes (0x0004, "XAdvance", 0, 1), 1305*e1fe3e4aSElliott Hughes (0x0008, "YAdvance", 0, 1), 1306*e1fe3e4aSElliott Hughes (0x0010, "XPlaDevice", 1, 0), 1307*e1fe3e4aSElliott Hughes (0x0020, "YPlaDevice", 1, 0), 1308*e1fe3e4aSElliott Hughes (0x0040, "XAdvDevice", 1, 0), 1309*e1fe3e4aSElliott Hughes (0x0080, "YAdvDevice", 1, 0), 1310*e1fe3e4aSElliott Hughes # reserved: 1311*e1fe3e4aSElliott Hughes (0x0100, "Reserved1", 0, 0), 1312*e1fe3e4aSElliott Hughes (0x0200, "Reserved2", 0, 0), 1313*e1fe3e4aSElliott Hughes (0x0400, "Reserved3", 0, 0), 1314*e1fe3e4aSElliott Hughes (0x0800, "Reserved4", 0, 0), 1315*e1fe3e4aSElliott Hughes (0x1000, "Reserved5", 0, 0), 1316*e1fe3e4aSElliott Hughes (0x2000, "Reserved6", 0, 0), 1317*e1fe3e4aSElliott Hughes (0x4000, "Reserved7", 0, 0), 1318*e1fe3e4aSElliott Hughes (0x8000, "Reserved8", 0, 0), 1319*e1fe3e4aSElliott Hughes] 1320*e1fe3e4aSElliott Hughes 1321*e1fe3e4aSElliott Hughes 1322*e1fe3e4aSElliott Hughesdef _buildDict(): 1323*e1fe3e4aSElliott Hughes d = {} 1324*e1fe3e4aSElliott Hughes for mask, name, isDevice, signed in valueRecordFormat: 1325*e1fe3e4aSElliott Hughes d[name] = mask, isDevice, signed 1326*e1fe3e4aSElliott Hughes return d 1327*e1fe3e4aSElliott Hughes 1328*e1fe3e4aSElliott Hughes 1329*e1fe3e4aSElliott HughesvalueRecordFormatDict = _buildDict() 1330*e1fe3e4aSElliott Hughes 1331*e1fe3e4aSElliott Hughes 1332*e1fe3e4aSElliott Hughesclass ValueRecordFactory(object): 1333*e1fe3e4aSElliott Hughes """Given a format code, this object convert ValueRecords.""" 1334*e1fe3e4aSElliott Hughes 1335*e1fe3e4aSElliott Hughes def __init__(self, valueFormat): 1336*e1fe3e4aSElliott Hughes format = [] 1337*e1fe3e4aSElliott Hughes for mask, name, isDevice, signed in valueRecordFormat: 1338*e1fe3e4aSElliott Hughes if valueFormat & mask: 1339*e1fe3e4aSElliott Hughes format.append((name, isDevice, signed)) 1340*e1fe3e4aSElliott Hughes self.format = format 1341*e1fe3e4aSElliott Hughes 1342*e1fe3e4aSElliott Hughes def __len__(self): 1343*e1fe3e4aSElliott Hughes return len(self.format) 1344*e1fe3e4aSElliott Hughes 1345*e1fe3e4aSElliott Hughes def readValueRecord(self, reader, font): 1346*e1fe3e4aSElliott Hughes format = self.format 1347*e1fe3e4aSElliott Hughes if not format: 1348*e1fe3e4aSElliott Hughes return None 1349*e1fe3e4aSElliott Hughes valueRecord = ValueRecord() 1350*e1fe3e4aSElliott Hughes for name, isDevice, signed in format: 1351*e1fe3e4aSElliott Hughes if signed: 1352*e1fe3e4aSElliott Hughes value = reader.readShort() 1353*e1fe3e4aSElliott Hughes else: 1354*e1fe3e4aSElliott Hughes value = reader.readUShort() 1355*e1fe3e4aSElliott Hughes if isDevice: 1356*e1fe3e4aSElliott Hughes if value: 1357*e1fe3e4aSElliott Hughes from . import otTables 1358*e1fe3e4aSElliott Hughes 1359*e1fe3e4aSElliott Hughes subReader = reader.getSubReader(value) 1360*e1fe3e4aSElliott Hughes value = getattr(otTables, name)() 1361*e1fe3e4aSElliott Hughes value.decompile(subReader, font) 1362*e1fe3e4aSElliott Hughes else: 1363*e1fe3e4aSElliott Hughes value = None 1364*e1fe3e4aSElliott Hughes setattr(valueRecord, name, value) 1365*e1fe3e4aSElliott Hughes return valueRecord 1366*e1fe3e4aSElliott Hughes 1367*e1fe3e4aSElliott Hughes def writeValueRecord(self, writer, font, valueRecord): 1368*e1fe3e4aSElliott Hughes for name, isDevice, signed in self.format: 1369*e1fe3e4aSElliott Hughes value = getattr(valueRecord, name, 0) 1370*e1fe3e4aSElliott Hughes if isDevice: 1371*e1fe3e4aSElliott Hughes if value: 1372*e1fe3e4aSElliott Hughes subWriter = writer.getSubWriter() 1373*e1fe3e4aSElliott Hughes writer.writeSubTable(subWriter, offsetSize=2) 1374*e1fe3e4aSElliott Hughes value.compile(subWriter, font) 1375*e1fe3e4aSElliott Hughes else: 1376*e1fe3e4aSElliott Hughes writer.writeUShort(0) 1377*e1fe3e4aSElliott Hughes elif signed: 1378*e1fe3e4aSElliott Hughes writer.writeShort(value) 1379*e1fe3e4aSElliott Hughes else: 1380*e1fe3e4aSElliott Hughes writer.writeUShort(value) 1381*e1fe3e4aSElliott Hughes 1382*e1fe3e4aSElliott Hughes 1383*e1fe3e4aSElliott Hughesclass ValueRecord(object): 1384*e1fe3e4aSElliott Hughes # see ValueRecordFactory 1385*e1fe3e4aSElliott Hughes 1386*e1fe3e4aSElliott Hughes def __init__(self, valueFormat=None, src=None): 1387*e1fe3e4aSElliott Hughes if valueFormat is not None: 1388*e1fe3e4aSElliott Hughes for mask, name, isDevice, signed in valueRecordFormat: 1389*e1fe3e4aSElliott Hughes if valueFormat & mask: 1390*e1fe3e4aSElliott Hughes setattr(self, name, None if isDevice else 0) 1391*e1fe3e4aSElliott Hughes if src is not None: 1392*e1fe3e4aSElliott Hughes for key, val in src.__dict__.items(): 1393*e1fe3e4aSElliott Hughes if not hasattr(self, key): 1394*e1fe3e4aSElliott Hughes continue 1395*e1fe3e4aSElliott Hughes setattr(self, key, val) 1396*e1fe3e4aSElliott Hughes elif src is not None: 1397*e1fe3e4aSElliott Hughes self.__dict__ = src.__dict__.copy() 1398*e1fe3e4aSElliott Hughes 1399*e1fe3e4aSElliott Hughes def getFormat(self): 1400*e1fe3e4aSElliott Hughes format = 0 1401*e1fe3e4aSElliott Hughes for name in self.__dict__.keys(): 1402*e1fe3e4aSElliott Hughes format = format | valueRecordFormatDict[name][0] 1403*e1fe3e4aSElliott Hughes return format 1404*e1fe3e4aSElliott Hughes 1405*e1fe3e4aSElliott Hughes def getEffectiveFormat(self): 1406*e1fe3e4aSElliott Hughes format = 0 1407*e1fe3e4aSElliott Hughes for name, value in self.__dict__.items(): 1408*e1fe3e4aSElliott Hughes if value: 1409*e1fe3e4aSElliott Hughes format = format | valueRecordFormatDict[name][0] 1410*e1fe3e4aSElliott Hughes return format 1411*e1fe3e4aSElliott Hughes 1412*e1fe3e4aSElliott Hughes def toXML(self, xmlWriter, font, valueName, attrs=None): 1413*e1fe3e4aSElliott Hughes if attrs is None: 1414*e1fe3e4aSElliott Hughes simpleItems = [] 1415*e1fe3e4aSElliott Hughes else: 1416*e1fe3e4aSElliott Hughes simpleItems = list(attrs) 1417*e1fe3e4aSElliott Hughes for mask, name, isDevice, format in valueRecordFormat[:4]: # "simple" values 1418*e1fe3e4aSElliott Hughes if hasattr(self, name): 1419*e1fe3e4aSElliott Hughes simpleItems.append((name, getattr(self, name))) 1420*e1fe3e4aSElliott Hughes deviceItems = [] 1421*e1fe3e4aSElliott Hughes for mask, name, isDevice, format in valueRecordFormat[4:8]: # device records 1422*e1fe3e4aSElliott Hughes if hasattr(self, name): 1423*e1fe3e4aSElliott Hughes device = getattr(self, name) 1424*e1fe3e4aSElliott Hughes if device is not None: 1425*e1fe3e4aSElliott Hughes deviceItems.append((name, device)) 1426*e1fe3e4aSElliott Hughes if deviceItems: 1427*e1fe3e4aSElliott Hughes xmlWriter.begintag(valueName, simpleItems) 1428*e1fe3e4aSElliott Hughes xmlWriter.newline() 1429*e1fe3e4aSElliott Hughes for name, deviceRecord in deviceItems: 1430*e1fe3e4aSElliott Hughes if deviceRecord is not None: 1431*e1fe3e4aSElliott Hughes deviceRecord.toXML(xmlWriter, font, name=name) 1432*e1fe3e4aSElliott Hughes xmlWriter.endtag(valueName) 1433*e1fe3e4aSElliott Hughes xmlWriter.newline() 1434*e1fe3e4aSElliott Hughes else: 1435*e1fe3e4aSElliott Hughes xmlWriter.simpletag(valueName, simpleItems) 1436*e1fe3e4aSElliott Hughes xmlWriter.newline() 1437*e1fe3e4aSElliott Hughes 1438*e1fe3e4aSElliott Hughes def fromXML(self, name, attrs, content, font): 1439*e1fe3e4aSElliott Hughes from . import otTables 1440*e1fe3e4aSElliott Hughes 1441*e1fe3e4aSElliott Hughes for k, v in attrs.items(): 1442*e1fe3e4aSElliott Hughes setattr(self, k, int(v)) 1443*e1fe3e4aSElliott Hughes for element in content: 1444*e1fe3e4aSElliott Hughes if not isinstance(element, tuple): 1445*e1fe3e4aSElliott Hughes continue 1446*e1fe3e4aSElliott Hughes name, attrs, content = element 1447*e1fe3e4aSElliott Hughes value = getattr(otTables, name)() 1448*e1fe3e4aSElliott Hughes for elem2 in content: 1449*e1fe3e4aSElliott Hughes if not isinstance(elem2, tuple): 1450*e1fe3e4aSElliott Hughes continue 1451*e1fe3e4aSElliott Hughes name2, attrs2, content2 = elem2 1452*e1fe3e4aSElliott Hughes value.fromXML(name2, attrs2, content2, font) 1453*e1fe3e4aSElliott Hughes setattr(self, name, value) 1454*e1fe3e4aSElliott Hughes 1455*e1fe3e4aSElliott Hughes def __ne__(self, other): 1456*e1fe3e4aSElliott Hughes result = self.__eq__(other) 1457*e1fe3e4aSElliott Hughes return result if result is NotImplemented else not result 1458*e1fe3e4aSElliott Hughes 1459*e1fe3e4aSElliott Hughes def __eq__(self, other): 1460*e1fe3e4aSElliott Hughes if type(self) != type(other): 1461*e1fe3e4aSElliott Hughes return NotImplemented 1462*e1fe3e4aSElliott Hughes return self.__dict__ == other.__dict__ 1463