Example #1
0
 def fromvalidatedwalker(cls, w, **kwArgs):
     """
     Creates and returns a new Version object from the specified walker,
     doing validation.
     
     >>> bs = bytes.fromhex("00020001")
     >>> obj = Version.fromvalidatedbytes(bs)
     version - DEBUG - Walker has 4 remaining bytes.
     
     >>> obj.pprint()
     Major version: 2
     Minor version: 1
     
     >>> Version.fromvalidatedbytes(bs[:-1])
     version - DEBUG - Walker has 3 remaining bytes.
     version - ERROR - Insufficient bytes.
     """
     
     if 'logger' in kwArgs:
         logger = kwArgs.pop('logger').getChild('version')
     else:
         logger = utilities.makeDoctestLogger('version')
     
     logger.debug((
       'V0001',
       int(w.length()),
       "Walker has %d remaining bytes."))
     
     if w.length() < 4:
         logger.error(('V0004', (), "Insufficient bytes."))
         return None
     
     t = w.group("H", 2)
     return cls(major=t[0], minor=t[1])
 def fromvalidatedwalker(cls, w, **kwArgs):
     """
     Creates and returns a new AxialCoordinate object from the data in the
     specified walker, doing validation. If a logger is not provided, one
     will be made.
     
     >>> bs = utilities.fromhex("40 00")
     >>> obj = AxialCoordinate.fromvalidatedbytes(bs)
     coord - DEBUG - Walker has 2 remaining bytes.
     coord - DEBUG - Coordinate value is 16384
     
     >>> obj == 1.0
     True
     
     >>> AxialCoordinate.fromvalidatedbytes(bs[:1])
     coord - DEBUG - Walker has 1 remaining bytes.
     coord - ERROR - Insufficient bytes.
     """
     
     if 'logger' in kwArgs:
         logger = kwArgs.pop('logger').getChild('coord')
     else:
         logger = utilities.makeDoctestLogger('coord')
     
     logger.debug((
       'V0001',
       int(w.length()),
       "Walker has %d remaining bytes."))
     
     if w.length() < 2:
         logger.error(('V0004', (), "Insufficient bytes."))
         return None
     
     raw = w.unpack("h")
     
     logger.debug((
       'Vxxxx',
       (raw,),
       "Coordinate value is %d"))
     
     return cls(raw / 16384)  # we've imported division from __future__
    def fromvalidatedwalker(cls, w, **kwArgs):
        """
        Creates and returns a new AxialCoordinates object from the specified
        walker, doing validation. The caller must provide an 'axisOrder' kwArg.
        
        >>> ao = ('wght', 'wdth')
        >>> obj = AxialCoordinates((AC(1.5), AC(-0.25)), axisOrder=ao)
        >>> bs = obj.binaryString()
        >>> fvb = AxialCoordinates.fromvalidatedbytes
        >>> obj2 = fvb(bs, axisOrder=ao)
        coords - DEBUG - Walker has 4 remaining bytes.
        coords.wght.coord - DEBUG - Walker has 4 remaining bytes.
        coords.wght.coord - DEBUG - Coordinate value is 24576
        coords.wdth.coord - DEBUG - Walker has 2 remaining bytes.
        coords.wdth.coord - DEBUG - Coordinate value is -4096
        coords - DEBUG - Coordinate is (wght 1.5, wdth -0.25)
        
        >>> obj2 == obj
        True
        
        >>> fvb(bs[:-1], axisOrder=ao)
        coords - DEBUG - Walker has 3 remaining bytes.
        coords.wght.coord - DEBUG - Walker has 3 remaining bytes.
        coords.wght.coord - DEBUG - Coordinate value is 24576
        coords.wdth.coord - DEBUG - Walker has 1 remaining bytes.
        coords.wdth.coord - ERROR - Insufficient bytes.
        
        Note that validation here catches cases where one or more axis tags are
        duplicated:
        
        >>> aoBad = ('wght', 'wght')
        >>> fvb(bs, axisOrder=aoBad)
        coords - DEBUG - Walker has 4 remaining bytes.
        coords - ERROR - There are duplicate axis tags
        """

        if 'logger' in kwArgs:
            logger = kwArgs.pop('logger').getChild('coords')
        else:
            logger = utilities.makeDoctestLogger('coords')

        logger.debug(
            ('V0001', int(w.length()), "Walker has %d remaining bytes."))

        ao = kwArgs['axisOrder']  # the absence is a code error, not a font one

        if len(ao) != len(set(ao)):
            logger.error(('V1034', (), "There are duplicate axis tags"))

            return None

        v = []
        fvw = AC.fromvalidatedwalker

        for axisTag in ao:
            subLogger = logger.getChild(axisTag)
            obj = fvw(w, logger=subLogger)

            if obj is None:
                return None

            v.append(obj)

        r = cls(v, axisOrder=ao)

        logger.debug(('Vxxxx', (r, ), "Coordinate is %s"))

        return r
Example #4
0
def analyzeFPGM(fpgmObj, **kwArgs):
    """
    Returns a dict mapping FDEF index numbers to tuples (see below). This
    code takes care of the details of doing analysis of non-CALL guys
    first.
    
    Note that the Hints objects in the Fpgm object will not be h2 Hints,
    but are old hints_tt Hints; this code handles that.
    
    The returned value is either a dict (if the useDictForm kwArg is True), or
    a tuple (if it is False, the default, for historical reasons). The per-FPGM
    tuple has the following form:
    
        [0]     dIn             A tuple of kinds of input arguments. This
                                comprises a series of (stack index, kind)
                                pairs.
        
        [1]     inCount         The number of input arguments. Note that this
                                might not necessarily be the length of dIn!
        
        [2]     inDeepest       The deepest level of popping that is attained
                                by this FDEF. Because of intrinsic operations,
                                this might be considerably larger than either
                                len(dIn) or inCount.
        
        [3]     ops             Some operations (like MINDEX) can leave the
                                stack in a state that can't be emulated by a
                                simple series of pops and pushes. In this case
                                this value, ops, will be a tuple of commands
                                needed to adjust the stack after the hint
                                executes. For instance, a sample value of ops
                                might be (('delete', -3, -2),), meaning the
                                [-3:-2] slice should be deleted in order to
                                bring the stack into agreement with how the
                                hint would have left things.
        
        [4]     dOut            A tuple of kinds of output arguments. As with
                                dIn, this comprises a series of (stack index,
                                kind) pairs.
        
        [5]     outCount        The number of output arguments. As with inCount
                                this might be different than len(dOut)!
    """

    if 'logger' in kwArgs:
        logger = kwArgs.pop('logger')
    else:
        logger = utilities.makeDoctestLogger("analyzeFPGM", level=50)

    fb = hints_tt.Hints.frombytes
    f2 = {}

    for n, h in fpgmObj.items():
        bsOld = h.binaryString()

        if bsOld == _fdef17_orig:
            bsOld = _fdef17_rewrite

            logger.info(
                ('Vxxxx', (), "Rewrote FDEF 17 to remove computed CALL"))

        elif bsOld == _fdef44_bad:
            bsOld = _fdef44_good

            logger.info(('Vxxxx', (), "Corrected erroneous FDEF 44"))

        f2[n] = fb(bsOld, infoString=h.infoString)

    rTemp = {}
    r = {}

    # do a separate first pass for non-CALLers; then loop...

    thisRound = set()
    badSet = {0x1C, 0x2A, 0x2B, 0x78, 0x79}

    for n, h in f2.items():
        if all(op.isPush or (op not in badSet) for op in h):
            thisRound.add(n)

    if not thisRound:
        logger.error(('Vxxxx', (), "All FDEFs call other FDEFs"))

        return None

    hadInconsistencies = False

    for n in sorted(thisRound):
        fictons = f2[n].removeIFs()

        logger.info(('Vxxxx', (n, len(fictons)),
                     "FDEF %d disassembles into %d ficton(s)"))

        consistencySet = set()

        for i, h in enumerate(fictons):
            obj = Analyzer(h)
            t = obj.run()
            consistencySet.add((t[1], t[5]))
            rTemp[(n, i)] = t

        if len(consistencySet) > 1:
            logger.error(('V1071', (
                n,
                sorted(consistencySet),
            ), "Different fictons for FDEF %d yield different numbers of "
                          "inputs and outputs: %s"))

            hadInconsistencies = True

        del f2[n]

    if hadInconsistencies:
        return None

    _mergeAnalysis(rTemp, r, logger)

    # Now that we have the stack implications for the FDEFs without CALLs, go
    # through and use this as the callInfo to finish off the rest of the FDEFs.

    badSet.discard(0x2A)
    badSet.discard(0x2B)

    while f2:
        # A call to run() will return None if unknown CALLs are there.
        thisRound = set()

        for n, h in f2.items():
            if all(op.isPush or (op not in badSet) for op in h):
                thisRound.add(n)

        if not thisRound:
            logger.error(
                ('Vxxxx', (), "All remaining FDEFs call other FDEFs that "
                 "themselves call other FDEFs"))

            return None

        notYets = set()
        rTemp = {}
        hadInconsistencies = False

        for n in sorted(thisRound):
            fictons = f2[n].removeIFs()

            logger.info(('Vxxxx', (n, len(fictons)),
                         "FDEF %d disassembles into %d ficton(s)"))

            consistencySet = set()

            for i, h in enumerate(fictons):
                obj = Analyzer(h)
                runOut = obj.run(callInfo=r)

                if runOut is None:
                    notYets.add(n)

                    for k in list(rTemp):
                        if k[0] == n:
                            del rTemp[k]

                    break

                consistencySet.add((runOut[1], runOut[5]))
                rTemp[(n, i)] = runOut

            if len(consistencySet) > 1:
                logger.error(('V1071', (
                    n,
                    sorted(consistencySet),
                ), "Different fictons for FDEF %d yield different numbers of "
                              "inputs and outputs: %s"))

                hadInconsistencies = True

        if hadInconsistencies:
            return None

        thisRound -= notYets
        _mergeAnalysis(rTemp, r, logger)

        for n in thisRound:
            del f2[n]

    if kwArgs.get('useDictForm', False):
        d = {}

        for fdefIndex, t in r.items():
            inTuple, inCount, inDeepest, ops, outTuple, outCount = t

            if inCount:
                dIn = dict.fromkeys(range(inCount))

                for i, t in inTuple:
                    dIn[i] = t

            else:
                dIn = {}

            if outCount:
                dOut = dict.fromkeys(range(outCount))

                for i, t in inTuple:
                    dOut[i] = t

            else:
                dOut = {}

            d[fdefIndex] = (dIn, inCount, inDeepest, ops, dOut, outCount)

        r = d

    return r
Example #5
0
    def fromvalidatedwalker(cls, w, **kwArgs):
        """
        """

        if 'logger' in kwArgs:
            logger = kwArgs.pop('logger').getChild('cvar')
        else:
            logger = utilities.makeDoctestLogger('cvar')

        logger.debug(
            ('V0001', int(w.length()), "Walker has %d remaining bytes."))

        e = kwArgs['editor']

        if not e.reallyHas('fvar'):
            logger.error(
                ('V1037', (),
                 "Font has a 'cvar' table but no 'fvar' table; this is "
                 "not a valid state."))

            return None

        fvarObj = e.fvar
        ao = fvarObj.axisOrder
        tableSize = w.length()

        if tableSize < 8:
            logger.error(('V0004', (), "Insufficient bytes."))
            return None

        vers, tupleCount, dataOffset = w.unpack("L2H")

        if vers != cls.VERSION:
            logger.error(
                ('V1051', (cls.VERSION, vers),
                 "Was expecting version 0x%08X, but got 0x%08X instead."))

            return None

        if tupleCount & 0x8000:
            logger.error(
                ('V1052', (),
                 "The 'cvar' table's tupleCount field has the 0x8000 bit set. "
                 "This is not support for 'cvar' (only for 'gvar')."))

            return None

        overhead = 8 + (4 + 2 * len(ao)) * tupleCount

        if dataOffset < overhead or dataOffset > tableSize:
            logger.error(
                ('V1054', (dataOffset, ),
                 "The dataOffset value 0x%04X appears to be invalid."))

            return None

        tupleHeaders = []
        afvw = axial_coordinates.AxialCoordinates.fromvalidatedwalker

        for i in range(tupleCount):
            subLogger = logger.getChild("tuple header %d" % (i, ))

            if w.length() < 4:
                logger.error(('V0004', (), "Insufficient bytes."))
                return None

            tupleSize, tupleIndex = w.unpack("2H")

            if (tupleIndex & 0xF000) != 0xA000:
                subLogger.error((
                    'V1053', (tupleIndex & 0xF000, ),
                    "Expected tupleIndex flags to be 0xA000, but got 0x%04X."))

                return None

            coords = afvw(w, axisOrder=ao, logger=subLogger)
            tupleHeaders.append((tupleSize, coords))

        wData = w.subWalker(dataOffset)
        r = cls({}, axisOrder=ao)
        DD = deltadict.DeltaDict

        for tupleSize, coords in tupleHeaders:
            subLogger = logger.getChild("coord %s" % (coords, ))

            if wData.length() < tupleSize:
                logger.error(('V0004', (), "Insufficient bytes."))
                return None

            startOffset = wData.getOffset()
            cvtIndices = cls._unpackCVTs_validated(wData, e, subLogger)

            if cvtIndices is None:
                return None

            deltas = cls._unpackDeltas_validated(wData, cvtIndices, subLogger)

            if deltas is None:
                return None

            alignSkip = (wData.getOffset() - startOffset) - tupleSize

            for cvtIndex, dlt in zip(cvtIndices, deltas):
                if cvtIndex not in r:
                    r[cvtIndex] = DD()

                r[cvtIndex][coords] = dlt

            if alignSkip:
                wData.skip(alignSkip)

        return r
Example #6
0
    def fromvalidatedwalker(cls, w, **kwArgs):
        """
        Creates and returns a new Avar object from the data in the specified
        walker, doing validation. Caller must provide either an 'axisOrder' or
        an 'editor' keyword argument.
        
        >>> logger = utilities.makeDoctestLogger('fvw')
        >>> AD = axisdict.AxisDict
        >>> ad1 = AD({-1.0: -1.0, 0.0: 0.0, 0.5: 0.25, 1.0: 1.0})
        >>> ad2 = AD({-1.0: -1.0, -0.75: -0.25, 0.0: 0.0, 0.5: 0.75, 1.0: 1.0})
        >>> ao = ('wght', 'wdth')
        >>> obj = Avar({'wght': ad1, 'wdth': ad2}, axisOrder=ao)
        >>> bs = obj.binaryString()
        >>> obj2 = Avar.fromvalidatedbytes(bs, logger=logger, axisOrder=ao)
        fvw.axisdict - DEBUG - Walker has 48 remaining bytes.
        fvw.axisdict.wght.axisdict - DEBUG - Walker has 40 remaining bytes.
        fvw.axisdict.wdth.axisdict - DEBUG - Walker has 22 remaining bytes.
        >>> obj == obj2
        True
        >>> bs = utilities.fromhex("0001 0000 0005 0009 C000 C000 0000 0000")
        >>> obj3 = Avar.fromvalidatedbytes(bs, logger=logger, axisOrder=ao)
        fvw.axisdict - DEBUG - Walker has 16 remaining bytes.
        fvw.axisdict - ERROR - Reserved field is 0x0005; expected 0.
        fvw.axisdict - ERROR - The axisCount 9 does not match axisCount 2 from the 'fvar' table.
        fvw.axisdict.wght.axisdict - DEBUG - Walker has 8 remaining bytes.
        fvw.axisdict.wght.axisdict - ERROR - Insufficient bytes.
        fvw.axisdict.wdth.axisdict - DEBUG - Walker has 6 remaining bytes.
        fvw.axisdict.wdth.axisdict - ERROR - Insufficient bytes.
        >>> obj4 = Avar.fromvalidatedbytes(b'ABC', logger=logger)
        fvw.axisdict - DEBUG - Walker has 3 remaining bytes.
        fvw.axisdict - ERROR - Insufficient bytes.
        >>> bs = utilities.fromhex("0123 4567 0000 FFFF")
        >>> obj5 = Avar.fromvalidatedbytes(bs, logger=logger, axisOrder=ao)
        fvw.axisdict - DEBUG - Walker has 8 remaining bytes.
        fvw.axisdict - ERROR - Was expecting 'avar' version 0x00010000 but got 0x01234567 instead.
        """

        if 'logger' in kwArgs:
            logger = kwArgs.pop('logger').getChild('axisdict')
        else:
            logger = utilities.makeDoctestLogger('axisdict')

        logger.debug(
            ('V0001', int(w.length()), "Walker has %d remaining bytes."))

        if w.length() < 8:
            logger.error(('V0004', (), "Insufficient bytes."))
            return None

        assert ('axisOrder' in kwArgs) or ('editor' in kwArgs)

        if 'axisOrder' in kwArgs:
            ao = tuple(kwArgs['axisOrder'])
        else:
            ao = tuple(kwArgs['editor'].fvar.axisOrder)

        vers, reserved, count = w.unpack("LHH")

        if vers != cls.VERSION:
            logger.error(('Vxxxx', (cls.VERSION, vers),
                          "Was expecting 'avar' version 0x%08X but got "
                          "0x%08X instead."))

            return None

        if reserved != 0:
            logger.error(('Vxxxx', (reserved, ),
                          "Reserved field is 0x%04X; expected 0."))

        if count != len(ao):
            logger.error(('Vxxxx', (
                count, len(ao)
            ), "The axisCount %d does not match axisCount %d from the 'fvar' table."
                          ))

        r = cls({}, axisOrder=ao)
        fvw = axisdict.AxisDict.fromvalidatedwalker

        for axisTag in ao:
            subLogger = logger.getChild(axisTag)
            r[axisTag] = fvw(w, logger=subLogger, **kwArgs)

        return r
Example #7
0
    def fromvalidatedwalker(cls, w, **kwArgs):
        """
        """

        if 'logger' in kwArgs:
            logger = kwArgs.pop('logger').getChild('gvar')
        else:
            logger = utilities.makeDoctestLogger('gvar')

        logger.debug(
            ('V0001', int(w.length()), "Walker has %d remaining bytes."))

        e = kwArgs['editor']

        if not e.reallyHas('fvar'):
            logger.error(
                ('V1037', (),
                 "Font has a 'gvar' table but no 'fvar' table; this is "
                 "not a valid state."))

            return None

        fvarObj = e.fvar
        ao = fvarObj.axisOrder
        r = cls(axisOrder=ao)
        origTableSize = w.length()

        if origTableSize < 20:
            logger.error(('V0004', (), "Insufficient bytes."))
            return None

        t = w.unpack("L2HL2HL")
        vers, axisCount, coordCount, coordOff, glyphCount, flags, dataOff = t
        logger.debug(('Vxxxx', (vers, ), "Version is 0x%08X"))
        logger.debug(('Vxxxx', (axisCount, ), "Axis count is %d"))
        logger.debug(('Vxxxx', (coordCount, ), "Global coord count is %d"))
        logger.debug(('Vxxxx', (coordOff, ), "Global coord offset is 0x%08X"))
        logger.debug(('Vxxxx', (glyphCount, ), "Glyph count is %d"))
        logger.debug(('Vxxxx', (flags, ), "Flags is 0x%04X"))
        logger.debug(('Vxxxx', (dataOff, ), "Data offset is 0x%08X"))

        if vers != r.VERSION:
            logger.error(('V1038', (
                r.VERSION,
                vers,
            ), "Unknown 'gvar' version; was expecting 0x%08X but "
                          "got 0x%08X instead."))

            return None

        if axisCount != len(ao):
            logger.error(
                ('V1039', (len(ao), axisCount),
                 "Mismatch between the axis counts in 'fvar' (%d) and "
                 "'gvar' (%d)."))

            return None

        if glyphCount != utilities.getFontGlyphCount(**kwArgs):
            logger.error(
                ('V1040', (),
                 "Mismatch between the 'maxp' and 'gvar' glyph counts."))

            return None

        if flags not in {0, 1}:
            logger.warning(
                ('V1041', (flags, ),
                 "The 'gvar' flags value is 0x%04X; this includes bits "
                 "that are not defined. These will be ignored."))

        # Check the offsets for reasonableness

        offsetSize = (4 if (flags & 1) else 2)

        if coordCount:
            globStop = coordOff + (coordCount * len(ao) * 2)
            lowBounds = 20 + (glyphCount + 1) * offsetSize

            if (coordOff < lowBounds) or (globStop > origTableSize):
                logger.error(('V1042', (
                ), "The 'gvar' global coordinates do not fit within the bounds "
                              "of the entire 'gvar' table."))

                return None

        if (dataOff < 20) or (dataOff >= origTableSize):
            logger.error(
                ('V1043', (),
                 "The 'gvar' variations data starting offset is outside "
                 "the bounds of the entire 'gvar' table"))

            return None

        # Read the global coordinates (if any)

        globalCoords = []

        if coordCount:
            wSub = w.subWalker(coordOff)
            fvw = axial_coordinates.AxialCoordinates.fromvalidatedwalker
            globalCoords = [None] * coordCount

            for i in range(coordCount):
                subLogger = logger.getChild("globalCoord %d" % (i, ))
                obj = fvw(wSub, axisOrder=ao, logger=subLogger)

                if obj is None:
                    return None

                globalCoords[i] = obj

        # Get the offsets to the actual glyph variation data

        if w.length() < offsetSize * (glyphCount + 1):
            logger.error(('V1043', (),
                          "The 'gvar' glyph variation array extends past the "
                          "table boundaries."))

            return None

        offsets = w.group("HL"[flags & 1], glyphCount + 1)

        if offsetSize == 2:
            offsets = [2 * n for n in offsets]

        if list(offsets) != sorted(offsets):
            logger.error(
                ('V1044', (),
                 "The per-glyph offsets are not monotonically increasing."))

            return None

        for glyphIndex, (off1, off2) in enumerate(utilities.pairwise(offsets)):
            w.align(2)

            if off1 == off2:
                continue

            # For now I'm assuming the presence of a 'glyf' table...

            glyphObj = e.glyf[glyphIndex]

            if glyphObj.isComposite:
                pointCount = len(glyphObj.components)
            else:
                pointCount = glyphObj.pointCount()

            wSub = w.subWalker(off1 + dataOff)
            subLogger = logger.getChild("glyph %d" % (glyphIndex, ))

            wasOK = r._fillGlyph_validated(wSub,
                                           glyphIndex,
                                           globalCoords,
                                           pointCount,
                                           logger=subLogger,
                                           **kwArgs)

            if not wasOK:
                return None

        return r
Example #8
0
    def fromvalidatedwalker(cls, w, **kwArgs):
        """
        Creates and returns a Format2 object from the specified walker, doing
        source validation. The walker should start at the subtable header. The
        following keyword arguments are suggested (if they are not present, the
        default values for coverage and tupleIndex will be used, which won't
        usually be what's wanted):
        
            coverage    A Coverage object.
            logger      A logger to which messages will be posted.
            tupleIndex  The variations tuple index.
        
        >>> s = _testingValues[1].binaryString()
        >>> logger = utilities.makeDoctestLogger("fvw")
        >>> fvb = Format2.fromvalidatedbytes
        >>> obj = fvb(s, logger=logger)
        fvw.format2 - DEBUG - Walker has 94 remaining bytes.
        fvw.format2.left.lookup_aat - DEBUG - Walker has 78 remaining bytes.
        fvw.format2.left.lookup_aat.binsearch.binsrch header - DEBUG - Walker has 76 remaining bytes.
        fvw.format2.right.lookup_aat - DEBUG - Walker has 50 remaining bytes.
        fvw.format2.right.lookup_aat.binsearch.binsrch header - DEBUG - Walker has 48 remaining bytes.
        
        >>> fvb(s[:1], logger=logger)
        fvw.format2 - DEBUG - Walker has 1 remaining bytes.
        fvw.format2 - ERROR - Insufficient bytes.
        
        >>> fvb(s[:-1], logger=logger)
        fvw.format2 - DEBUG - Walker has 93 remaining bytes.
        fvw.format2.left.lookup_aat - DEBUG - Walker has 77 remaining bytes.
        fvw.format2.left.lookup_aat.binsearch.binsrch header - DEBUG - Walker has 75 remaining bytes.
        fvw.format2.right.lookup_aat - DEBUG - Walker has 49 remaining bytes.
        fvw.format2.right.lookup_aat.binsearch.binsrch header - DEBUG - Walker has 47 remaining bytes.
        fvw.format2 - ERROR - The array is missing or incomplete.
        """

        if 'logger' in kwArgs:
            logger = kwArgs.pop('logger').getChild('format2')
        else:
            logger = utilities.makeDoctestLogger('format2')

        logger.debug(
            ('V0001', int(w.length()), "Walker has %d remaining bytes."))

        kwArgs.pop('fontGlyphCount', None)

        if w.length() < 16:
            logger.error(('V0004', (), "Insufficient bytes."))
            return None

        headerAdj = 12
        rowBytes = w.unpack("L")

        if not rowBytes:
            logger.warning(
                ('V0605', (),
                 "The rowBytes value is zero, so there is no table data."))

            return cls({}, **utilities.filterKWArgs(cls, kwArgs))

        nCols = rowBytes // 2
        leftOffset, rightOffset, arrayBase = w.unpack("3L")

        # Determine nRows, by finding the largest offset present in the left
        # class table.

        if leftOffset < headerAdj:
            logger.error(('V0607', (leftOffset, ),
                          "The leftOffset value %d is too small."))

            return None

        wLeft = w.subWalker(leftOffset - headerAdj)

        leftLookup = lookup.Lookup.fromvalidatedwalker(
            wLeft, logger=logger.getChild("left"), **kwArgs)

        if leftLookup is None:
            return None  # error will already have been logged

        bad = {i for i in leftLookup.values() if i % rowBytes}

        if bad:
            logger.error(
                ('V0612', (sorted(bad), ),
                 "The following left-class values are not multiples of "
                 "the rowBytes value: %s"))

            return None

        leftCD = classdef.ClassDef(
            {i: j // rowBytes
             for i, j in leftLookup.items()})

        usedRows = set(leftCD.values())
        nRows = 1 + max(usedRows)
        unusedRows = (set(range(nRows)) - usedRows) - {0}

        if unusedRows:
            logger.warning(('Vxxxx', (sorted(unusedRows), ),
                            "The following rows are never referred to by any "
                            "left-hand entry: %s"))

        if rightOffset < headerAdj:
            logger.error(('V0614', (rightOffset, ),
                          "The rightOffset value %d is too small."))

            return None

        wRight = w.subWalker(rightOffset - headerAdj)

        rightLookup = lookup.Lookup.fromvalidatedwalker(
            wRight, logger=logger.getChild("right"), **kwArgs)

        if rightLookup is None:
            return None  # error will already have been logged

        bad = {i for i in rightLookup.values() if i % 2}

        if bad:
            logger.error((
                'V0615', (sorted(bad), ),
                "The following right-class values are not multiples of 2: %s"))

            return None

        rightCD = classdef.ClassDef(
            {i: j // 2
             for i, j in rightLookup.items()})

        usedCols = set(rightCD.values())
        unusedCols = (set(range(nCols)) - usedCols) - {0}

        if unusedCols:
            logger.warning(
                ('Vxxxx', (sorted(unusedCols), ),
                 "The following columns are never referred to by any "
                 "right-hand entry: %s"))

        if arrayBase < headerAdj:
            logger.error(('V0609', (arrayBase, ),
                          "The arrayBase value %d is too small."))

            return None

        wArray = w.subWalker(arrayBase - headerAdj)

        if wArray.length() < 2 * nCols * nRows:
            logger.error(('V0610', (), "The array is missing or incomplete."))

            return None

        arrayData = wArray.group("h", nCols * nRows)
        leftUniques = set(leftLookup.values())
        rightUniques = set(rightLookup.values())

        for delKey in {'leftClassDef', 'rightClassDef'}:
            kwArgs.pop(delKey, None)

        r = cls({},
                leftClassDef=leftCD,
                rightClassDef=rightCD,
                **utilities.filterKWArgs(cls, kwArgs))

        CP = classpair.ClassPair

        for uLeft in leftUniques:
            rowIndex = uLeft // rowBytes

            for uRight in rightUniques:
                dlt = arrayData[(uLeft + uRight) // 2]

                if dlt:
                    r[CP([rowIndex, uRight // 2])] = dlt

        return r
Example #9
0
 def fromvalidatedwalker(cls, w, **kwArgs):
     """
     Creates and returns a new PackedPoints object from the specified
     walker, doing validation. The 'pointCount' keyword argument is
     required, and should just be the actual point count, not including the
     4 phantom points.
     
     >>> obj = PackedPoints((3, 4, 5, 19, 20, 31, 40, 41, 42, 43))
     >>> obj.pprint()
     3-5, 19-20, 31, 40-43
     
     >>> bs = obj.binaryString(pointCount=80)
     >>> obj2 = PackedPoints.fromvalidatedbytes(bs, pointCount=80)
     points - DEBUG - Walker has 12 remaining bytes.
     points - DEBUG - Points are [3, 4, 5, 19, 20, 31, 40, 41, 42, 43]
     
     >>> PackedPoints.fromvalidatedbytes(bs[:-1], pointCount=80)
     points - DEBUG - Walker has 11 remaining bytes.
     points - ERROR - The packed point array ends prematurely for runCount 10 and format 'B' (expectedSize 10, available 9).
     
     >>> PackedPoints.fromvalidatedbytes(bs, pointCount=20)
     points - DEBUG - Walker has 12 remaining bytes.
     points - ERROR - The unpacked point array contains point indices that are too large for the specified number of points in the glyph.
     
     >>> PackedPoints.fromvalidatedbytes(bs, pointCount=4)
     points - DEBUG - Walker has 12 remaining bytes.
     points - ERROR - The specified number of points (10) exceeds the number of points in the specified glyph.
     """
     
     if 'logger' in kwArgs:
         logger = kwArgs.pop('logger').getChild('points')
     else:
         logger = utilities.makeDoctestLogger('points')
     
     logger.debug((
       'V0001',
       int(w.length()),
       "Walker has %d remaining bytes."))
     
     pointCount = kwArgs['pointCount']
     
     if w.length() < 1:
         logger.error(('V0004', (), "Insufficient bytes."))
         return None
     
     totalPoints = w.unpack("B")
     
     if not totalPoints:
         logger.debug(('Vxxxx', (), "Using all points in the glyph"))
         return cls(range(pointCount + 4))
     
     if totalPoints > 127:
         if w.length() < 1:
             logger.error(('V0004', (), "Insufficient bytes."))
             return None
         
         totalPoints = ((totalPoints - 128) * 256) + w.unpack("B")
     
     if totalPoints > (pointCount + 4):
         logger.error((
           'V0135',
           (totalPoints,),
           "The specified number of points (%d) exceeds the number "
           "of points in the specified glyph."))
         
         return None
     
     pointIndices = []
     
     while totalPoints:
         if w.length() < 1:
             logger.error((
               'V1036',
               (),
               "The packed point array is empty."))
             
             return None
         
         runCount = w.unpack("B")
         fmt = "BH"[runCount > 127]
         runCount = (runCount & 0x7F) + 1
         expectedSize = w.calcsize("%d%s" % (runCount, fmt))
         
         if w.length() < expectedSize:
             logger.error((
               'V1036',
               (runCount, fmt, expectedSize, int(w.length())),
               "The packed point array ends prematurely for "
               "runCount %d and format '%s' (expectedSize %d, "
               "available %d)."))
             
             return None
         
         # The data here are not point indices, but packed point indices,
         # stored as deltas from the previous point (except the first).
         
         for n in w.group(fmt, runCount):
             if not pointIndices:
                 pointIndices.append(n)
             else:
                 pointIndices.append(pointIndices[-1] + n)
         
         totalPoints -= runCount
     
     if pointIndices[-1] >= pointCount + 4:
         logger.error((
           'V1035',
           (),
           "The unpacked point array contains point indices that are too "
           "large for the specified number of points in the glyph."))
         
         return None
     
     logger.debug(('Vxxxx', (pointIndices,), "Points are %s"))
     return cls(pointIndices)