def _getUSpan(key, onlyPrintables=False):
    """
    Given a bit number as the key, returns a Span with the Unicodes in the full
    version of that code page. If onlyPrintable is True, the Unicodes with
    property 'Cc' (i.e. control characters) will be filtered out of the result.
    
    >>> _printUSpan(_getUSpan(0))  # Latin 1
    U+0000 through U+007F
    U+00A0 through U+00FF
    U+0152 through U+0153
    U+0160 through U+0161
    U+0178
    U+017D through U+017E
    U+0192
    U+02C6
    U+02DC
    U+2013 through U+2014
    U+2018 through U+201A
    U+201C through U+201E
    U+2020 through U+2022
    U+2026
    U+2030
    U+2039 through U+203A
    U+20AC
    U+2122
    
    >>> _printUSpan(_getUSpan(0, True))  # Latin 1, printables only
    U+0020 through U+007E
    U+00A0 through U+00FF
    U+0152 through U+0153
    U+0160 through U+0161
    U+0178
    U+017D through U+017E
    U+0192
    U+02C6
    U+02DC
    U+2013 through U+2014
    U+2018 through U+201A
    U+201C through U+201E
    U+2020 through U+2022
    U+2026
    U+2030
    U+2039 through U+203A
    U+20AC
    U+2122
    """

    enc, is16Bit, name = _bitData[key]
    r = span2.Span(ord(c) for c in str(_b8BitSmall, enc, errors='ignore'))
    r.update({ord(c) for c in str(_b16BitBig, enc, errors='ignore')})

    if onlyPrintables:
        uc = unicodedata.category
        f = (lambda c: uc(chr(c)) == 'Cc')
        r = span2.Span(itertools.filterfalse(f, r))

    return r
Exemple #2
0
    def _subdivide(self, keepZeroes=False):
        """
        Returns a list with (first, last) pairs of keys that are contiguous and
        whose associated glyphs are monotonic.
        
        >>> obj = Format4({50: 4, 51: 5, 52: 6, 53: 10, 54: 11, 60: 12})
        >>> obj._subdivide()
        [(50, 52), (53, 54), (60, 60)]
        """

        r = []
        mgg = utilities.monotonicGroupsGenerator

        for uFirst, uLast in span2.Span(self).ranges():
            gVec = [self[u] for u in range(uFirst, uLast + 1)]
            pieceIndex = 0

            for piece in mgg(gVec, allowZeroes=keepZeroes):
                pieceLen = piece[1] - piece[0]

                r.append(
                    (uFirst + pieceIndex, uFirst + pieceIndex + pieceLen - 1))

                pieceIndex += pieceLen

        return r
Exemple #3
0
def _validate(obj, **kwArgs):
    logger = kwArgs.pop('logger')
    r = True

    # gather together all glyphs that refer to a given entry, so we only need
    # to put one message out for the whole lot, instead of per-glyph messages.

    idToImmut = {}
    immutToObjSpan = {}

    for glyphIndex, csmEntry in obj.items():
        entryID = id(csmEntry)

        if entryID not in idToImmut:
            idToImmut[entryID] = csmEntry.asImmutable()

        immut = idToImmut[entryID]

        if immut not in immutToObjSpan:
            immutToObjSpan[immut] = (csmEntry, span2.Span())

        immutToObjSpan[immut][1].add(glyphIndex)

    for csmEntry, s in immutToObjSpan.values():
        subLogger = logger.getChild("glyphs %s" % (s, ))
        r = csmEntry.isValid(logger=subLogger, **kwArgs) and r

    return r
Exemple #4
0
    def buildBinary(self, w, **kwArgs):
        """
        Adds the binary content for ``self`` to the specified writer.
    
        :param w: A :py:class:`~fontio3.utilities.writer.LinkedWriter`
        :param kwArgs: Optional keyword arguments (there are none here)
        :return: None
        :raises ValueError: if one or more values cannot fit into four bytes
        
        >>> utilities.hexdump(_testingValues[0].binaryString())
               0 | 000C 0000 0000 0010  0000 0000 0000 0000 |................|
        
        >>> utilities.hexdump(_testingValues[1].binaryString())
               0 | 000C 0000 0000 001C  0000 0000 0000 0001 |................|
              10 | 0000 3A00 0000 3A00  0000 0019           |..:...:.....    |
        
        >>> utilities.hexdump(_testingValues[2].binaryString())
               0 | 000C 0000 0000 0028  0000 0000 0000 0002 |.......(........|
              10 | 0000 3A00 0000 3A01  0000 0019 0000 3A02 |..:...:.......:.|
              20 | 0000 3A02 0000 001D                      |..:.....        |
        
        >>> utilities.hexdump(_testingValues[3].binaryString())
               0 | 000C 0000 0000 0034  0000 0000 0000 0003 |.......4........|
              10 | 0000 3A00 0000 3A01  0000 0019 0000 3A02 |..:...:.......:.|
              20 | 0000 3A02 0000 001D  000A 2318 000A 2318 |..:.......#...#.|
              30 | 0000 001B                                |....            |
        """

        self._preBuildValidate()
        startLength = w.byteLength
        w.add("HH", 12, 0)  # format and pad
        lengthStake = w.addDeferredValue("L")
        w.add("L", (self.language or 0))
        groups = []

        if self:
            mgg = utilities.monotonicGroupsGenerator
            sp = span2.Span(self)

            for g in sp.ranges():
                if g[1] < 0x7FFFFFFE:
                    vC = [x for x in range(g[0], g[1] + 1)]
                else:
                    vC = list(range(g[0], g[1] + 1))

                charIndex = 0
                itSub = mgg(self[c] for c in vC)

                for glyphStart, glyphStopPlusOne, ignore in itSub:
                    thisLen = glyphStopPlusOne - glyphStart

                    groups.append((vC[charIndex], vC[charIndex + thisLen - 1],
                                   glyphStart))

                    charIndex += thisLen

        w.add("L", len(groups))
        w.addGroup("3L", groups)
        w.setDeferredValue(lengthStake, "L", w.byteLength - startLength)
Exemple #5
0
    def __str__(self):
        """
        Returns a string representation of the object.
        
        >>> print(_testingValues[1])
        26-28, 258-259
        """

        return str(span2.Span(self))
Exemple #6
0
    def __str__(self):
        """
        Returns a compact representation of the glyph indices covered by the
        Groups object.
        
        >>> g = Groups()
        >>> g.update(range(100, 2000))
        >>> g.update(range(2200, 4000))
        >>> print(g)
        100-1999, 2200-3999
        """

        return str(span2.Span(self))
Exemple #7
0
    def buildBinary(self, w, **kwArgs):
        """
        Adds the binary data for the Groups to the specified LinkedWriter.
        
        >>> utilities.hexdump(_testingValues[0].binaryString())
               0 | 0000                                     |..              |
        
        >>> utilities.hexdump(_testingValues[1].binaryString())
               0 | 0005 0002 0004 0000  000A 000B 0000 001E |................|
              10 | 001F 0000 0021 0021  0000 0023 0027 0000 |.....!.!...#.'..|
        """

        s = span2.Span(self)
        groups = list(s.ranges())
        w.add("H", len(groups))
        w.addGroup("2Hxx", groups)
Exemple #8
0
    def buildBinary(self, w, **kwArgs):
        """
        Adds the binary content for ``self`` to the specified writer.
    
        :param w: A :py:class:`~fontio3.utilities.writer.LinkedWriter`
        :param kwArgs: Optional keyword arguments (see below)
        :return: None
        :raises ValueError: if one or more values cannot fit into two bytes
        
        The following ``kwArgs`` are supported:
        
        ``keepZeroGlyphUnicodes``
            If False (the default) entries explicitly mapping to glyph 0 will
            be removed, since the defined ``'cmap'`` behavior is to map all
            unmapped character codes to glyph 0 anyway. However, sometimes the
            explicit zero should be kept; if you want this, set this flag to
            True.
        
        >>> utilities.hexdump(_testingValues[0].binaryString())
               0 | 0004 0018 0000 0002  0002 0000 0000 FFFF |................|
              10 | 0000 FFFF 0001 0000                      |........        |
        
        >>> utilities.hexdump(_testingValues[1].binaryString())
               0 | 0004 0020 0000 0004  0004 0001 0000 002A |... ...........*|
              10 | FFFF 0000 002A FFFF  003A 0001 0000 0000 |.....*...:......|
        
        >>> utilities.hexdump(_testingValues[2].binaryString())
               0 | 0004 0020 0000 0004  0004 0001 0000 002B |... ...........+|
              10 | FFFF 0000 002A FFFF  003A 0001 0000 0000 |.....*...:......|
        
        >>> utilities.hexdump(_testingValues[3].binaryString())
               0 | 0004 0028 0000 0006  0004 0001 0002 002B |...(...........+|
              10 | 1234 FFFF 0000 002A  1234 FFFF 003A EDD1 |.4.....*.4...:..|
              20 | 0001 0000 0000 0000                      |........        |
        """

        keepZeroes = kwArgs.pop('keepZeroGlyphUnicodes', False)
        self._preBuildValidate()
        bs = self._buildBinary_sub(list(span2.Span(self).ranges()))

        if bs is None:
            bs = self._buildBinary_sub(self._subdivide(keepZeroes=keepZeroes))

        w.addString(bs)
def _validate(obj, **kwArgs):
    logger = kwArgs['logger']
    
    if 'unicodeSpan' in kwArgs:
        uSpan = kwArgs['unicodeSpan']
    
    else:
        e = kwArgs['editor']
        
        if e is None or not e.reallyHas(b'cmap'):
            logger.error((
              'V0553',
              (),
              "Unable to validate CodePageRanges because the Editor and/or "
              "the Cmap are not available."))
            
            return False
        
        uSpan = span2.Span(e.cmap.getUnicodeMap())
    
    d = obj.__dict__
    
    for key in set(_bitData):  # symbol is validated separately
        printablesMissing = _getUSpan(key, True) - uSpan
        
        if printablesMissing:
            # there are missing printables from the range
            if d[obj.rangeIDToName[key]]:
                logger.warning((
                  'W2101',
                  (_bitData[key][0],
                   ["U+%04X" % (n,) for n in sorted(printablesMissing)]),
                  "Code page %s is missing printables %s"))
        
        elif not d[obj.rangeIDToName[key]]:
            # all printables are present, but code page is off
            logger.warning((
              'W2100',
              (_bitData[key][0],),
              "Code page %s has all printables, but is set to False"))
    
    return True
Exemple #10
0
class Groups(set, metaclass=setmeta.FontDataMetaclass):
    """
    Sets gathering glyph indices.
    
    >>> _testingValues[1].pprint()
    2-4, 10-11, 30-31, 33, 35-39
    """

    #
    # Class definition variables
    #

    setSpec = dict(item_renumberdirect=True,
                   set_pprintfunc=(lambda p, obj: p(str(span2.Span(obj)))))

    #
    # Methods
    #

    def __str__(self):
        """
        Returns a compact representation of the glyph indices covered by the
        Groups object.
        
        >>> g = Groups()
        >>> g.update(range(100, 2000))
        >>> g.update(range(2200, 4000))
        >>> print(g)
        100-1999, 2200-3999
        """

        return str(span2.Span(self))

    def buildBinary(self, w, **kwArgs):
        """
        Adds the binary data for the Groups to the specified LinkedWriter.
        
        >>> utilities.hexdump(_testingValues[0].binaryString())
               0 | 0000                                     |..              |
        
        >>> utilities.hexdump(_testingValues[1].binaryString())
               0 | 0005 0002 0004 0000  000A 000B 0000 001E |................|
              10 | 001F 0000 0021 0021  0000 0023 0027 0000 |.....!.!...#.'..|
        """

        s = span2.Span(self)
        groups = list(s.ranges())
        w.add("H", len(groups))
        w.addGroup("2Hxx", groups)

    @classmethod
    def from20(cls, g):
        """
        Returns a Groups from a version 2.0 Groups. The keys of the 2.0 Groups
        are used, and nothing else.
        """

        return cls(g)

    @classmethod
    def fromvalidatedwalker(cls, w, **kwArgs):
        """
        Creates and returns a Group object from the specified walker, doing
        source validation.
        
        >>> s = _testingValues[1].binaryString()
        >>> logger = utilities.makeDoctestLogger("groups15_fvw")
        >>> fvb = Groups.fromvalidatedbytes
        >>> obj = fvb(s, logger=logger)
        groups15_fvw.groups15 - DEBUG - Walker has 32 remaining bytes.
        groups15_fvw.groups15 - INFO - The group index array is sorted by start glyph.
        
        >>> fvb(s[:1], logger=logger)
        groups15_fvw.groups15 - DEBUG - Walker has 1 remaining bytes.
        groups15_fvw.groups15 - ERROR - Insufficient bytes.
        
        >>> fvb(s[:-1], logger=logger)
        groups15_fvw.groups15 - DEBUG - Walker has 31 remaining bytes.
        groups15_fvw.groups15 - ERROR - The group index array is missing or incomplete.
        
        >>> obj = fvb(s[:23] + utilities.fromhex("27") + s[24:], logger=logger)
        groups15_fvw.groups15 - DEBUG - Walker has 32 remaining bytes.
        groups15_fvw.groups15 - INFO - The group index array is sorted by start glyph.
        groups15_fvw.groups15 - WARNING - Some of the group index records overlap (that is, some glyphs are members of more than one record).
        """

        logger = kwArgs.pop('logger', logging.getLogger())
        logger = logger.getChild("groups15")

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

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

        count = w.unpack("H")

        if w.length() < 6 * count:
            logger.error(('V0571', (),
                          "The group index array is missing or incomplete."))

            return None

        rawList = w.group("2H2x", count)

        if list(rawList) != sorted(rawList):
            logger.info(
                ('V0572', (),
                 "The group index array is not sorted by start glyph."))

        else:
            logger.info(('V0573', (),
                         "The group index array is sorted by start glyph."))

        rawCount = sum(t[1] - t[0] + 1 for t in rawList)
        theSpan = span2.Span.fromranges(rawList)

        if len(theSpan) != rawCount:
            logger.warning(('V0574', (
            ), "Some of the group index records overlap (that is, some glyphs "
                            "are members of more than one record)."))

        return cls(theSpan)

    @classmethod
    def fromwalker(cls, w, **kwArgs):
        """
        Creates and returns a Groups from the specified walker.
        
        >>> fb = Groups.frombytes
        >>> _testingValues[0] == fb(_testingValues[0].binaryString())
        True
        
        >>> _testingValues[1] == fb(_testingValues[1].binaryString())
        True
        """

        count = w.unpack("H")
        rawList = w.group("2H2x", count)
        theSpan = span2.Span.fromranges(rawList)
        return cls(theSpan)
Exemple #11
0
    def findSizeSharers(self):
        """
        Find potential re-use cases across PPEMs. This method returns a dict
        whose keys are (ppem, ppem+1) tuples and whose values are Spans with
        all the glyphs whose images are identical in those two ppems.
        
        This information is necessary, but not sufficient. The sharing can
        really only happen for monospace ranges (since the non-monospace ranges
        include advance in their metrics, which are not shared). So the dict
        returned by this method needs to be intersected (as it were) with the
        monospace glyph ranges, as it only applies there.
        """

        # make the dict of image hashes
        dSizeGlyphToHash = {}

        for ppem, stk in self.items():
            d = dSizeGlyphToHash.setdefault(ppem, {})

            for glyphIndex, data in stk.items():
                if data is not None and data.imageFormat == 5:
                    d[glyphIndex] = hash(tuple(tuple(v) for v in data.image))

        # walk the dict
        sortedSizes = sorted(dSizeGlyphToHash)
        dOutput = {}  # (smaller, larger) -> set of glyphs

        for smaller, larger in zip(sortedSizes, sortedSizes[1:]):
            if ((smaller[0] != (larger[0] - 1)) or (smaller[1] !=
                                                    (larger[1] - 1))
                    or (smaller[2] != larger[2])):

                continue

            thisSet = (smaller, larger)
            d = dOutput.setdefault(thisSet, set())
            dSmaller = dSizeGlyphToHash[smaller]
            dLarger = dSizeGlyphToHash[larger]
            commonKeys = set(dSmaller) & set(dLarger)

            for key in commonKeys:
                if dSmaller[key] == dLarger[key]:

                    # I tested tens of thousands of glyphs at lots of PPEMs and
                    # the hash test suffices; image comparison isn't needed.

                    d.add(key)

        toDel = set()

        for key in dOutput:
            d = dOutput[key]

            if d:
                dOutput[key] = span2.Span(d)
            else:
                toDel.add(key)

        for key in toDel:
            del dOutput[key]

        return dOutput
Exemple #12
0
    def iterateSize(self, ppemKey, **kwArgs):
        """
        Given a key (xPPEM, yPPEM, depth), returns a generator over tuples of
        the form (firstGlyph, lastGlyph, indexFormat, datOffset) for the
        entries matching that key. The tuples are yielded sorted in firstGlyph
        order. Note that there may be holes between firstGlyph and lastGlyph.
        
        The following keyword arguments are used:
            
            datOffset       A list of length one, containing the offset to the
                            next available dat offset. It's done this way so
                            any changes to this value can be communicated back
                            to the caller.
            
            datPool         A dict mapping ppemKeys to dicts, which in turn map
                            glyph indices to base offsets in the related data
                            table ('bdat' or 'EBDT').
        
            forApple        If the forApple flag is True, the index formats
                            will be limited to 2 and 3. If the flag is False
                            (the default), index formats 2 through 5 will be
                            used. Index format 1 is never used, as the size is
                            too big.
            
            sizeSharers     The output from a call to the Sbit object's
                            findSizeSharers() method. If not specified, an
                            empty dict will be used.
        """

        forApple = kwArgs.get('forApple', False)
        sizeSharers = kwArgs.get('sizeSharers', {})
        datPool = kwArgs.get('datPool', {})
        datOffset = kwArgs.get('datOffset', [0])
        r = []

        for k, g in itertools.groupby(self[ppemKey],
                                      key=operator.itemgetter(2)):

            v = list(g)

            if k[0] is None:
                # it's a non-monospace group
                if len(v) == 1:
                    # emit a single index3 group
                    r.append((v[0][0], v[0][1], 3))
                    continue

                gapList = []

                for thisEntry, nextEntry in zip(v, v[1:]):
                    gapList.append(thisEntry[1] - thisEntry[0] + 1)
                    gapList.append(nextEntry[0] - thisEntry[1] - 1)

                gapList.append(v[-1][1] - v[-1][0] + 1)
                currSize = ('3', 18 + 2 * gapList[0])
                i = 3

                while i <= len(gapList):
                    vSub = gapList[:i]
                    size_3 = 18 + 2 * sum(vSub)
                    size_3s = sum(18 + 2 * n for n in vSub[0::2])
                    size_prior_plus_3 = currSize[1] + (18 + 2 * vSub[-1])

                    if not forApple:
                        size_4 = 20 + 4 * sum(vSub[0::2])
                        best = min(size_3, size_4, size_3s, size_prior_plus_3)

                    else:
                        best = min(size_3, size_3s, size_prior_plus_3)

                    if size_3 == best:
                        currSize = ('3', size_3)
                        i += 2

                    elif (not forApple) and (size_4 == best):
                        currSize = ('4', size_4)
                        i += 2

                    elif size_3s == best:
                        currSize = ('3s', size_3s)
                        i += 2

                    else:

                        # If we get here, we yield the current cumulation as a
                        # block and then start a new cumulation.

                        i -= 1  # now indexes first of new gapList

                        if currSize[0] == '3':
                            r.append((v[0][0], v[(i // 2) - 1][1], 3))

                        elif currSize[0] == '4':

                            # It never makes sense to split an index4 group, so
                            # if the last group added was a 4, just meld them.

                            if r and r[-1][-1] == 4:
                                oldLast = r.pop()
                                r.append((oldLast[0], v[(i // 2) - 1][1], 4))

                            else:
                                r.append((v[0][0], v[(i // 2) - 1][1], 4))

                        else:
                            assert currSize[0] == '3s'

                            for t in v[0:(i // 2)]:
                                r.append((t[0], t[1], 3))

                        gapList = gapList[i:]
                        v = v[(i // 2):]
                        currSize = ('3', 18 + 2 * gapList[0])
                        i = 3

                        if len(v) == 1:
                            break

                # Handle the final block

                i -= 1

                if currSize[0] == '3':
                    r.append((v[0][0], v[(i // 2) - 1][1], 3))

                elif currSize[0] == '4':
                    if r and r[-1][-1] == 4:
                        oldLast = r.pop()
                        r.append((oldLast[0], v[(i // 2) - 1][1], 4))

                    else:
                        r.append((v[0][0], v[(i // 2) - 1][1], 4))

                else:
                    assert currSize[0] == '3s'

                    for t in v[0:(i // 2)]:
                        r.append((t[0], t[1], 3))

            else:
                # it's a monospace group
                nGroups = len(v)
                nGlyphs = sum(t[1] - t[0] + 1 for t in v)

                if forApple or (((14 * nGroups) - 16) < nGlyphs):
                    for t in v:
                        r.append((t[0], t[1], 2))

                else:
                    r.append((v[0][0], v[-1][1], 5))

        sbitObj = self.sbitObj
        stk = sbitObj[ppemKey]
        dp = datPool.setdefault(ppemKey, {})

        for t in r:

            # Before yielding the entries, we need to do some checks for size
            # limitations of the corresponding bitmaps (since we never want to
            # have to use index format 1). We'll subdivide ranges where needed.

            actuals = span2.Span(stk.actualIterator(*t[:2]))
            ppemSmallerKey = (ppemKey[0] - 1, ppemKey[1] - 1, ppemKey[2])
            bSizes = {i: stk[i].binarySize() for i in actuals}

            if t[2] in {2, 5}:

                # Index formats 2 and 5 are used with monospace ranges, so we
                # need to check for sharing of data with the next smaller PPEM.

                assert len(set(iter(bSizes.values()))) == 1
                imageSize = bSizes[next(iter(actuals))]  # constant for mono
                ssKey = (ppemSmallerKey, ppemKey)

                if ssKey in sizeSharers:
                    ssSmaller = sizeSharers[ssKey]
                    dpSmaller = datPool[ppemSmallerKey]  # has to be there

                    # The following list contains tuples. For tuples of shared
                    # images, there are 5 elements:
                    #
                    #       True,
                    #       firstGlyph,
                    #       lastGlyph,
                    #       firstGlyph's datOffset
                    #       lastGlyph's datOffset
                    #
                    # For tuples of non-shared images, there are only 4
                    # elements per tuple:
                    #
                    #       False,
                    #       firstGlyph,
                    #       lastGlyph,
                    #       firstGlyph's datOffset

                    v = []

                    for glyph in sorted(actuals):
                        if not v:
                            if glyph in ssSmaller:
                                v.append((True, glyph, glyph, dpSmaller[glyph],
                                          dpSmaller[glyph]))

                                dp[glyph] = dpSmaller[glyph]

                            else:
                                v.append((False, glyph, glyph, datOffset[0]))
                                dp[glyph] = datOffset[0]
                                datOffset[0] += imageSize

                        else:
                            tLast = v[-1]

                            if glyph in ssSmaller:
                                if tLast[0]:
                                    expectNewOffset = tLast[-1] + imageSize

                                    if expectNewOffset == dpSmaller[glyph]:
                                        v[-1] = (True, tLast[1], glyph,
                                                 tLast[3], expectNewOffset)

                                    else:
                                        v.append((True, glyph, glyph,
                                                  dpSmaller[glyph],
                                                  dpSmaller[glyph]))

                                else:
                                    v.append(
                                        (True, glyph, glyph, dpSmaller[glyph],
                                         dpSmaller[glyph]))

                                dp[glyph] = dpSmaller[glyph]

                            else:
                                if tLast[0]:
                                    v.append(
                                        (False, glyph, glyph, datOffset[0]))

                                else:
                                    v[-1] = ((False, tLast[1], glyph,
                                              tLast[3]))

                                dp[glyph] = datOffset[0]
                                datOffset[0] += imageSize

                    # Note that in the following, I'm not doing anything to
                    # check to see if there was an untoward proliferation of
                    # little blocks, in an alternating "sharing/not-sharing"
                    # configuration. At some point, if that case arises often
                    # enough, we can optimize for it.

                    for tSub in v:
                        yield (tSub[1], tSub[2], t[2], tSub[3])

                else:
                    yield ((t[0], t[1], t[2], datOffset[0]))

                    for glyph in sorted(actuals):
                        dp[glyph] = datOffset[0]
                        datOffset[0] += imageSize

            elif t[2] in {3, 4}:

                # Index formats 3 and 4 deals with non-monospace glyphs with an
                # offset per glyph (including adjacent identical offsets for
                # empty glyphs). Here we check that the offsets from a relative
                # zero (at the start of the dat group for this index3 entry)
                # don't exceed 64K bytes by the end. The group will be
                # subdivided if that happens (without optimization).

                if sum(bSizes[i] for i in actuals) < 65536:
                    yield ((t[0], t[1], t[2], datOffset[0]))

                    for glyph in sorted(actuals):
                        dp[glyph] = datOffset[0]
                        datOffset[0] += bSizes[glyph]

                else:  # we need to subdivide here
                    v = []
                    cumulSize = 0

                    for glyph in sorted(actuals):
                        thisSize = bSizes[glyph]

                        if not v:
                            v.append([glyph, glyph])
                            cumulSize = thisSize

                        elif cumulSize + thisSize > 65535:
                            v.append([glyph, glyph])
                            cumulSize = thisSize

                        else:
                            v[-1][1] = glyph
                            cumulSize += thisSize

                        dp[glyph] = datOffset[0]
                        datOffset[0] += thisSize

                    for firstGlyph, lastGlyph in v:
                        yield ((firstGlyph, lastGlyph, t[2], dp[firstGlyph]))
def _recalc_all(obj, **kwArgs):
    """
    Recalculates all the flag bits.
    
    Keyword arguments supported are:
    
        base1252        A Boolean which, if True, means the cpthreshold value
                        will be interpreted only with respect to those parts of
                        the code page's repertoire that do not overlap any part
                        of the CP1252 code page. If False, then the comparison
                        is on the whole set by itself. Default is True.
                    
                        Note that this flag has no effect for the CP1252 code
                        page itself, which is always just tested against the
                        cpthreshold value.
    
        cpthreshold     An integral value indicating what portion of a
                        particular code page's repertoire needs to be present
                        in unicodeSpan for that code page to be flagged as
                        present. The values are interpreted as follows (default
                        is 95):
                    
                        0       Any one of the code page's members must be
                                present.
                        
                        1-99    Interpreted as a percentage of the code
                                page's repertoire that must be present.
                        
                        100     100% of the code page's members must be
                                present in unicodeSpan.
        
        unicodeSpan     A Span object with the present Unicodes. If not
                        specified, the Span will be constructed from the editor
                        via the cmap.Cmap.getUnicodeMap() method. In this case,
                        if either the Editor or the Cmap are unavailable, an
                        exception (NoEditor or NoCmap) will be raised.
    """

    if 'unicodeSpan' in kwArgs:
        uSpan = kwArgs['unicodeSpan']

    else:
        e = kwArgs['editor']

        if e is None:
            raise NoEditor()

        if not e.reallyHas(b'cmap'):
            raise NoCmap()

        uSpan = span2.Span(e.cmap.getUnicodeMap())

        has30 = bool(e.cmap.findKeys(3, 0))

    cpthreshold = kwArgs.get('cpthreshold', 95)
    base1252 = kwArgs.get('base1252', True)
    newObj = obj.__copy__()
    d = newObj.__dict__

    for key in set(_bitData) | {31}:
        if key == 31:
            d['hasSymbol'] = has30

        else:
            name = _bitData[key][2]
            uSpanEnc = _getUSpan(key, onlyPrintables=True)

            if base1252 and (key != 0):
                uSpanEnc -= _getUSpan(0, onlyPrintables=True)

            if uSpanEnc:
                diff = uSpanEnc - uSpan
                uCount = len(uSpanEnc)
                dCount = len(diff)

                if cpthreshold == 100:
                    d[name] = not diff

                elif cpthreshold == 0:
                    d[name] = dCount != uCount

                else:
                    presentRatio = float(uCount - dCount) / float(uCount)
                    d[name] = presentRatio >= (cpthreshold / 100.0)

    return (newObj != obj), newObj
Exemple #14
0
class NoCenters(frozenset, metaclass=setmeta.FontDataMetaclass):
    """
    Objects representing collections of glyph indices for those glyphs not to
    be auto-centered.
    
    >>> nc = NoCenters(set(range(300, 1200)) - set({450}))
    >>> print(nc)
    300-449, 451-1199
    
    >>> d = dict(zip(range(300, 350), range(7000, 7050)))
    >>> nc.glyphsRenumbered(d).pprint(label="Glyph spans")
    Glyph spans:
      350-449, 451-1199, 7000-7049
    
    >>> r1 = set(range(100, 199, 3))
    >>> r2 = set(range(101, 200, 3))
    >>> NoCenters(r1 | r2).pprint(label="Ranges", maxWidth=75)
    Ranges:
      100-101, 103-104, 106-107, 109-110, 112-113, 115-116, 118-119, 121-122,
      124-125, 127-128, 130-131, 133-134, 136-137, 139-140, 142-143, 145-146,
      148-149, 151-152, 154-155, 157-158, 160-161, 163-164, 166-167, 169-170,
      172-173, 175-176, 178-179, 181-182, 184-185, 187-188, 190-191, 193-194,
      196-197
    """

    #
    # Class definition variables
    #

    setSpec = dict(item_renumberdirect=True,
                   set_pprintfunc=(lambda p, i: p(str(span2.Span(i)))))

    #
    # Methods
    #

    def __str__(self):
        """
        Returns a string representation of the object.
        
        >>> print(_testingValues[1])
        26-28, 258-259
        """

        return str(span2.Span(self))

    def buildBinary(self, w, **kwArgs):
        """
        Adds the binary data for the NoCenters object to the specified
        LinkedWriter.
        
        >>> utilities.hexdump(_testingValues[0].binaryString())
               0 | 0000                                     |..              |
        
        >>> utilities.hexdump(_testingValues[1].binaryString())
               0 | 0005 001A 001B 001C  0102 0103           |............    |
        """

        w.add("H", len(self))
        w.addGroup("H", sorted(self))

    @classmethod
    def fromvalidatedwalker(cls, w, **kwArgs):
        """
        Creates and returns a new NoCenter object from the specified walker,
        doing source validation.
        
        >>> s = _testingValues[1].binaryString()
        >>> logger = utilities.makeDoctestLogger("nocenters_test")
        >>> fvb = NoCenters.fromvalidatedbytes
        >>> print(fvb(s, logger=logger))
        nocenters_test.nocenters - DEBUG - Walker has 12 remaining bytes.
        26-28, 258-259
        
        >>> fvb(s[:1], logger=logger)
        nocenters_test.nocenters - DEBUG - Walker has 1 remaining bytes.
        nocenters_test.nocenters - ERROR - Insufficient bytes.
        
        >>> fvb(s[:-1], logger=logger)
        nocenters_test.nocenters - DEBUG - Walker has 11 remaining bytes.
        nocenters_test.nocenters - ERROR - There were not enough NoCenter glyphs to match the specified count (5).
        
        >>> print(fvb(utilities.fromhex("00 00"), logger=logger), end='')
        nocenters_test.nocenters - DEBUG - Walker has 2 remaining bytes.
        nocenters_test.nocenters - INFO - The NoCenter count is zero.
        
        >>> fvb(s[:4] + s[6:] + s[4:6], logger=logger)
        nocenters_test.nocenters - DEBUG - Walker has 12 remaining bytes.
        nocenters_test.nocenters - ERROR - The NoCenter list is not sorted by glyph index.
        """

        logger = kwArgs.pop('logger', logging.getLogger())
        logger = logger.getChild("nocenters")

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

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

        count = w.unpack("H")

        if not count:
            logger.info(('V0557', (), "The NoCenter count is zero."))

            return cls()

        if w.length() < 2 * count:
            logger.error(('V0556', (count, ),
                          "There were not enough NoCenter glyphs to match the "
                          "specified count (%d)."))

            return None

        v = list(w.group("H", count))

        if v != sorted(v):
            logger.error(('V0567', (),
                          "The NoCenter list is not sorted by glyph index."))

            return None

        return cls(v)

    @classmethod
    def fromwalker(cls, w, **kwArgs):
        """
        Creates and returns a new NoCenter object from the specified walker.
        
        >>> fb = NoCenters.frombytes
        >>> _testingValues[0] == fb(_testingValues[0].binaryString())
        True
        
        >>> _testingValues[1] == fb(_testingValues[1].binaryString())
        True
        """

        return cls(w.group("H", w.unpack("H")))
Exemple #15
0
 def buildBinary(self, w, **kwArgs):
     """
     Adds the binary content for ``self`` to the specified writer.
 
     :param w: A :py:class:`~fontio3.utilities.writer.LinkedWriter`
     :param kwArgs: Optional keyword arguments (see below)
     :return: None
     :raises ValueError: if no eligible subtable format can accommodate
         the data present in ``self``
     
     The following ``kwArgs`` are supported:
     
     ``stakeValue``
         A stake that will be placed at the start of the binary data laid
         down by this method. One will be synthesized if not provided.
         See :py:class:`~fontio3.utilities.writer.LinkedWriter` for a
         description of stakes and how they're used.
     
     >>> utilities.hexdump(_testingValues[1].binaryString())
            0 | 000E 0000 0023 0000  0001 00FE 0100 0000 |.....#..........|
           10 | 0000 0000 1500 0000  0200 614E 0017 0062 |..........aN...b|
           20 | 2600 61                                  |&.a             |
     
     >>> utilities.hexdump(_testingValues[2].binaryString())
            0 | 000E 0000 0041 0000  0002 00FE 0300 0000 |.....A..........|
           10 | 2000 0000 000E 0100  0000 002C 0000 0038 | ..........,...8|
           20 | 0000 0002 004E 0100  004E 0800 0000 0002 |.....N...N......|
           30 | 0052 2500 022F 4300  0000 0001 0089 EE00 |.R%../C.........|
           40 | 29                                       |)               |
     """
     
     if 'stakeValue' in kwArgs:
         stakeValue = kwArgs.pop('stakeValue')
         w.stakeCurrentWithValue(stakeValue)
     else:
         stakeValue = w.stakeCurrent()
     
     startLength = w.byteLength
     w.add("H", 14)  # format
     lengthStake = w.addDeferredValue("L")
     w.add("L", len(self))
     defStakes = {}
     nonDefStakes = {}
     
     for vs in sorted(self):
         w.add("T", vs)
         d = self[vs]
         
         if d.default:
             defStakes[vs] = w.getNewStake()
             w.addUnresolvedOffset("L", stakeValue, defStakes[vs])
         else:
             w.add("L", 0)
         
         if len(d):
             nonDefStakes[vs] = w.getNewStake()
             w.addUnresolvedOffset("L", stakeValue, nonDefStakes[vs])
         else:
             w.add("L", 0)
     
     for vs in sorted(self):
         d = self[vs]
         
         if vs in defStakes:
             w.stakeCurrentWithValue(defStakes[vs])
             s = span2.Span(d.default)
             count = 0
             countStake = w.addDeferredValue("L")
             
             for first, last in s.ranges():
                 spanCount = last - first + 1
                 
                 while spanCount:
                     pieceCount = min(256, spanCount)
                     w.add("TB", first, pieceCount - 1)
                     count += 1
                     first += pieceCount
                     spanCount -= pieceCount
             
             w.setDeferredValue(countStake, "L", count)
         
         if vs in nonDefStakes:
             w.stakeCurrentWithValue(nonDefStakes[vs])
             w.add("L", len(d))
             w.addGroup("TH", sorted(d.items(), key=operator.itemgetter(0)))
     
     #w.alignToByteMultiple(2)
     w.setDeferredValue(lengthStake, "L", int(w.byteLength - startLength))
Exemple #16
0
def _pprint(p, seq, **kwArgs):
    s = span2.Span(seq)
    p.simple(s, **kwArgs)
Exemple #17
0
    def buildBinary(self, w, **kwArgs):
        """
        Adds the binary content for ``self`` to the specified writer.
    
        :param w: A :py:class:`~fontio3.utilities.writer.LinkedWriter`
        :param kwArgs: Optional keyword arguments (there are none here)
        :return: None
        :raises ValueError: if one or more values cannot fit into four bytes
        
        >>> pp.PP().sequence_grouped(_testingValues[0].binaryString())
        [0]: 0
        [1]: 8
        [2-5]: 0
        [6]: 32
        [7]: 16
        [8-8207]: 0
        
        >>> pp.PP().sequence_grouped(_testingValues[1].binaryString())
        [0]: 0
        [1]: 8
        [2-5]: 0
        [6]: 32
        [7]: 28
        [8-11]: 0
        [12]: 32
        [13-8206]: 0
        [8207]: 1
        [8208]: 0
        [8209]: 2
        [8210-8212]: 0
        [8213]: 2
        [8214-8217]: 0
        [8218]: 11
        [8219]: 184
        
        >>> pp.PP().sequence_grouped(_testingValues[2].binaryString())
        [0]: 0
        [1]: 8
        [2-5]: 0
        [6]: 32
        [7]: 28
        [8-11]: 0
        [12]: 32
        [13-8206]: 0
        [8207]: 1
        [8208]: 0
        [8209]: 2
        [8210-8212]: 0
        [8213]: 2
        [8214]: 0
        [8215]: 1
        [8216-8217]: 0
        [8218]: 11
        [8219]: 184
        """

        self._preBuildValidate()
        startLength = w.byteLength
        w.add("HH", 8, 0)  # format and pad
        lengthStake = w.addDeferredValue("L")
        w.add("L", (self.language or 0))
        highSet = set(c >> 16 for c in self)
        v = [0] * 65536

        for i in highSet:
            v[i] = 1

        w.addString(utilitiesbackend.utImplode(v))
        countStake = w.addDeferredValue("L")
        count = 0

        for spanStart, spanEnd in span2.Span(self).ranges():
            if spanEnd < 0x7FFFFFFE:
                v = [self[i] for i in range(spanStart, spanEnd + 1)]
            else:
                v = [self[i] for i in range(spanStart, spanEnd + 1)]

            for gStart, gStop, ignore in utilities.monotonicGroupsGenerator(v):
                count += 1
                thisLen = gStop - gStart
                w.add("3L", spanStart, spanStart + thisLen - 1, gStart)
                spanStart += thisLen

        w.setDeferredValue(countStake, "L", count)
        w.setDeferredValue(lengthStake, "L", w.byteLength - startLength)
Exemple #18
0
def _validate(d, **kwArgs):
    editor = kwArgs['editor']
    scaler = kwArgs.get('scaler', None)
    logger = kwArgs['logger']

    logger = logger.getChild('vdmxrecord %d:%d' %
                             (d.ratio.xRatio, d.ratio.yEndRatio))

    if editor is None or (not editor.reallyHas(b'maxp')):
        logger.warning(
            ('W2503', (),
             "Unable to validate contents because there was no Editor "
             "or no 'maxp' table was available."))

        return False

    try:
        r = kwArgs.get('recalculateditem', None)
        diff = r != d

        if r is None:
            diff, r = _recalc(d, **kwArgs)

    except:
        logger.error(('V0554', (),
                      "An error occured in the scaler during device metrics "
                      "calculation, preventing validation."))

        return False

    if diff:
        fontGlyphCount = editor.maxp.numGlyphs

        for ppem, recalcrec in r.group.items():
            currec = d.group[ppem]

            if currec.yMax > recalcrec.yMax or currec.yMin < recalcrec.yMin:

                # VDMX is "too big"...only a warning. This is just
                # "inefficient"; it won't cause clipping.

                logger.warning(('W2500', (ppem, currec.yMax, currec.yMin,
                                          recalcrec.yMax, recalcrec.yMin),
                                "Record for %d ppem [%d,%d] is larger than "
                                "the calculated value [%d,%d]."))

            if currec.yMax < recalcrec.yMax or currec.yMin > recalcrec.yMin:

                # VDMX is "too small"...this is an error, as it will cause
                # clipping under Windows. If we have a scaler, report on
                # individual glyphs.

                if scaler is not None:
                    try:
                        ratio = d.ratio

                        if ratio.xRatio > 1 or ratio.yEndRatio > 1:
                            xs = (ratio.xRatio * 1.0) / (ratio.yEndRatio * 1.0)
                        else:
                            xs = 1

                        mat = matrix.Matrix(([xs * ppem, 0.0,
                                              0.0], [0.0, ppem * 1.0,
                                                     0.0], [0.0, 0.0, 1.0]))

                        try:
                            scaler.setTransform(mat)
                        except:
                            raise ScalerError()

                        badYMax = span2.Span()
                        badYMin = span2.Span()

                        for g in range(fontGlyphCount):
                            try:
                                m = scaler.getOutlineMetrics(g)
                            except:
                                raise ScalerError()

                            if oldRound(m.hi_y) > currec.yMax:
                                badYMax.add(g)

                            if oldRound(m.lo_y) < currec.yMin:
                                badYMin.add(g)

                        if len(badYMax) > 0:
                            logger.error(
                                ('V0775', (currec.yMax, ppem, str(badYMax)),
                                 "The following glyphs' actual yMax values "
                                 "exceed the stored value of %d at %d ppem: "
                                 "%s. Clipping may occur."))

                        if len(badYMin) > 0:
                            logger.error(
                                ('V0775', (currec.yMin, ppem, str(badYMin)),
                                 "The following glyphs' actual yMin values "
                                 "exceed the stored value of %d at %d ppem: "
                                 "%s. Clipping may occur."))

                    except ScalerError:
                        logger.error(
                            ('V0554', (),
                             "An error occured in the scaler during device "
                             "metrics calculation, preventing validation."))

                        return False

                # Otherwise just report generically for the ppem (using MS
                # error code).

                else:
                    logger.error(
                        ('E2500', (ppem, currec.yMax, currec.yMin,
                                   recalcrec.yMax, recalcrec.yMin),
                         "Record for %d ppem [%d,%d] is smaller than the "
                         "calculated value [%d,%d]. Clipping may occur."))
        return False

    logger.info(('P2500', (), "The VDMX data matches the expected values."))

    return True