示例#1
0
    def shorten_post(self, lines, w, h, margin=2):
        ''' Shortens the text to a single line according to the label options.

        This function operates on a text that has already been laid out because
        for markup, parts of text can have different size and options.

        If :attr:`text_size` [0] is None, the lines are returned unchanged.
        Otherwise, the lines are converted to a single line fitting within the
        constrained width, :attr:`text_size` [0].

        :params:

            `lines`: list of `LayoutLine` instances describing the text.
            `w`: int, the width of the text in lines, including padding.
            `h`: int, the height of the text in lines, including padding.
            `margin` int, the additional space left on the sides. This is in
            addition to :attr:`padding_x`.

        :returns:
            3-tuple of (xw, h, lines), where w, and h is similar to the input
            and contains the resulting width / height of the text, including
            padding. lines, is a list containing a single `LayoutLine`, which
            contains the words for the line.
        '''
        def n(line, c):
            ''' A function similar to text.find, except it's an iterator that
            returns successive occurrences of string c in list line. line is
            not a string, but a list of LayoutWord instances that we walk
            from left to right returning the indices of c in the words as we
            encounter them. Note that the options can be different among the
            words.

            :returns:
                3-tuple: the index of the word in line, the index of the
                occurrence in word, and the extents (width) of the combined
                words until this occurrence, not including the occurrence char.
                If no more are found it returns (-1, -1, total_w) where total_w
                is the full width of all the words.
            '''
            total_w = 0
            for w in range(len(line)):
                word = line[w]
                if not word.lw:
                    continue
                f = partial(word.text.find, c)
                i = f()
                while i != -1:
                    self.options = word.options
                    yield w, i, total_w + self.get_extents(word.text[:i])[0]
                    i = f(i + 1)
                self.options = word.options
                total_w += self.get_extents(word.text)[0]
            yield -1, -1, total_w  # this should never be reached, really

        def p(line, c):
            ''' Similar to the `n` function, except it returns occurrences of c
            from right to left in the list, line, similar to rfind.
            '''
            total_w = 0
            offset = 0 if len(c) else 1
            for w in range(len(line) - 1, -1, -1):
                word = line[w]
                if not word.lw:
                    continue
                f = partial(word.text.rfind, c)
                i = f()
                while i != -1:
                    self.options = word.options
                    yield (w, i,
                           total_w + self.get_extents(word.text[i + 1:])[0])
                    if i:
                        i = f(0, i - offset)
                    else:
                        if not c:
                            self.options = word.options
                            yield (w, -1,
                                   total_w + self.get_extents(word.text)[0])
                        break
                self.options = word.options
                total_w += self.get_extents(word.text)[0]
            yield -1, -1, total_w  # this should never be reached, really

        def n_restricted(line, uw, c):
            ''' Similar to the function `n`, except it only returns the first
            occurrence and it's not an iterator. Furthermore, if the first
            occurrence doesn't fit within width uw, it returns the index of
            whatever amount of text will still fit in uw.

            :returns:
                similar to the function `n`, except it's a 4-tuple, with the
                last element a boolean, indicating if we had to clip the text
                to fit in uw (True) or if the whole text until the first
                occurrence fitted in uw (False).
            '''
            total_w = 0
            if not len(line):
                return 0, 0, 0
            for w in range(len(line)):
                word = line[w]
                f = partial(word.text.find, c)
                self.options = word.options
                extents = self.get_cached_extents()
                i = f()
                if i != -1:
                    ww = extents(word.text[:i])[0]

                if i != -1 and total_w + ww <= uw:  # found and it fits
                    return w, i, total_w + ww, False
                elif i == -1:
                    ww = extents(word.text)[0]
                    if total_w + ww <= uw:  # wasn't found and all fits
                        total_w += ww
                        continue
                    i = len(word.text)

                # now just find whatever amount of the word does fit
                e = 0
                while e != i and total_w + extents(word.text[:e])[0] <= uw:
                    e += 1
                e = max(0, e - 1)
                return w, e, total_w + extents(word.text[:e])[0], True

            return -1, -1, total_w, False

        def p_restricted(line, uw, c):
            ''' Similar to `n_restricted`, except it returns the first
            occurrence starting from the right, like `p`.
            '''
            total_w = 0
            if not len(line):
                return 0, 0, 0
            for w in range(len(line) - 1, -1, -1):
                word = line[w]
                f = partial(word.text.rfind, c)
                self.options = word.options
                extents = self.get_cached_extents()
                i = f()
                if i != -1:
                    ww = extents(word.text[i + 1:])[0]

                if i != -1 and total_w + ww <= uw:  # found and it fits
                    return w, i, total_w + ww, False
                elif i == -1:
                    ww = extents(word.text)[0]
                    if total_w + ww <= uw:  # wasn't found and all fits
                        total_w += ww
                        continue

                # now just find whatever amount of the word does fit
                s = len(word.text) - 1
                while s >= 0 and total_w + extents(word.text[s:])[0] <= uw:
                    s -= 1
                return w, s, total_w + extents(word.text[s + 1:])[0], True

            return -1, -1, total_w, False

        textwidth = self.get_cached_extents()
        uw = self.text_size[0]
        if uw is None:
            return w, h, lines
        old_opts = copy(self.options)
        uw = max(0, int(uw - old_opts['padding_x'] * 2 - margin))
        chr = type(self.text)
        ssize = textwidth(' ')
        c = old_opts['split_str']
        line_height = old_opts['line_height']
        xpad, ypad = old_opts['padding_x'], old_opts['padding_y']
        dir = old_opts['shorten_from'][0]

        # flatten lines into single line
        line = []
        last_w = 0
        for l in range(len(lines)):
            # concatenate (non-empty) inside lines with a space
            this_line = lines[l]
            if last_w and this_line.w and not this_line.line_wrap:
                line.append(LayoutWord(old_opts, ssize[0], ssize[1], chr(' ')))
            last_w = this_line.w or last_w
            for word in this_line.words:
                if word.lw:
                    line.append(word)

        # if that fits, just return the flattened line
        lw = sum([word.lw for word in line])
        if lw <= uw:
            lh = max([word.lh for word in line] + [0]) * line_height
            self.is_shortened = False
            return (lw + 2 * xpad, lh + 2 * ypad,
                    [LayoutLine(0, 0, lw, lh, 1, 0, line)])

        elps_opts = copy(old_opts)
        if 'ellipsis_options' in old_opts:
            elps_opts.update(old_opts['ellipsis_options'])

        # Set new opts for ellipsis
        self.options = elps_opts
        # find the size of ellipsis that'll fit
        elps_s = textwidth('...')
        if elps_s[0] > uw:  # even ellipsis didn't fit...
            self.is_shortened = True
            s = textwidth('..')
            if s[0] <= uw:
                return (s[0] + 2 * xpad, s[1] * line_height + 2 * ypad, [
                    LayoutLine(0, 0, s[0], s[1], 1, 0,
                               [LayoutWord(old_opts, s[0], s[1], '..')])
                ])

            else:
                s = textwidth('.')
                return (s[0] + 2 * xpad, s[1] * line_height + 2 * ypad, [
                    LayoutLine(0, 0, s[0], s[1], 1, 0,
                               [LayoutWord(old_opts, s[0], s[1], '.')])
                ])

        elps = LayoutWord(elps_opts, elps_s[0], elps_s[1], '...')
        uw -= elps_s[0]
        # Restore old opts
        self.options = old_opts

        # now find the first left and right words that fit
        w1, e1, l1, clipped1 = n_restricted(line, uw, c)
        w2, s2, l2, clipped2 = p_restricted(line, uw, c)

        if dir != 'l':  # center or right
            line1 = None
            if clipped1 or clipped2 or l1 + l2 > uw:
                # if either was clipped or both don't fit, just take first
                if len(c):
                    self.options = old_opts
                    old_opts['split_str'] = ''
                    res = self.shorten_post(lines, w, h, margin)
                    self.options['split_str'] = c
                    self.is_shortened = True
                    return res
                line1 = line[:w1]
                last_word = line[w1]
                last_text = last_word.text[:e1]
                self.options = last_word.options
                s = self.get_extents(last_text)
                line1.append(
                    LayoutWord(last_word.options, s[0], s[1], last_text))
            elif (w1, e1) == (-1, -1):  # this shouldn't occur
                line1 = line
            if line1:
                line1.append(elps)
                lw = sum([word.lw for word in line1])
                lh = max([word.lh for word in line1]) * line_height
                self.options = old_opts
                self.is_shortened = True
                return (lw + 2 * xpad, lh + 2 * ypad,
                        [LayoutLine(0, 0, lw, lh, 1, 0, line1)])

            # now we know that both the first and last word fit, and that
            # there's at least one instances of the split_str in the line
            if (w1, e1) != (w2, s2):  # more than one split_str
                if dir == 'r':
                    f = n(line, c)  # iterator
                    assert next(f)[:-1] == (w1, e1)  # first word should match
                    ww1, ee1, l1 = next(f)
                    while l2 + l1 <= uw:
                        w1, e1 = ww1, ee1
                        ww1, ee1, l1 = next(f)
                        if (w1, e1) == (w2, s2):
                            break
                else:  # center
                    f = n(line, c)  # iterator
                    f_inv = p(line, c)  # iterator
                    assert next(f)[:-1] == (w1, e1)
                    assert next(f_inv)[:-1] == (w2, s2)
                    while True:
                        if l1 <= l2:
                            ww1, ee1, l1 = next(f)  # hypothesize that next fit
                            if l2 + l1 > uw:
                                break
                            w1, e1 = ww1, ee1
                            if (w1, e1) == (w2, s2):
                                break
                        else:
                            ww2, ss2, l2 = next(f_inv)
                            if l2 + l1 > uw:
                                break
                            w2, s2 = ww2, ss2
                            if (w1, e1) == (w2, s2):
                                break
        else:  # left
            line1 = [elps]
            if clipped1 or clipped2 or l1 + l2 > uw:
                # if either was clipped or both don't fit, just take last
                if len(c):
                    self.options = old_opts
                    old_opts['split_str'] = ''
                    res = self.shorten_post(lines, w, h, margin)
                    self.options['split_str'] = c
                    self.is_shortened = True
                    return res
                first_word = line[w2]
                first_text = first_word.text[s2 + 1:]
                self.options = first_word.options
                s = self.get_extents(first_text)
                line1.append(
                    LayoutWord(first_word.options, s[0], s[1], first_text))
                line1.extend(line[w2 + 1:])
            elif (w1, e1) == (-1, -1):  # this shouldn't occur
                line1 = line
            if len(line1) != 1:
                lw = sum([word.lw for word in line1])
                lh = max([word.lh for word in line1]) * line_height
                self.options = old_opts
                self.is_shortened = True
                return (lw + 2 * xpad, lh + 2 * ypad,
                        [LayoutLine(0, 0, lw, lh, 1, 0, line1)])

            # now we know that both the first and last word fit, and that
            # there's at least one instances of the split_str in the line
            if (w1, e1) != (w2, s2):  # more than one split_str
                f_inv = p(line, c)  # iterator
                assert next(f_inv)[:-1] == (w2, s2)  # last word should match
                ww2, ss2, l2 = next(f_inv)
                while l2 + l1 <= uw:
                    w2, s2 = ww2, ss2
                    ww2, ss2, l2 = next(f_inv)
                    if (w1, e1) == (w2, s2):
                        break

        # now add back the left half
        line1 = line[:w1]
        last_word = line[w1]
        last_text = last_word.text[:e1]
        self.options = last_word.options
        s = self.get_extents(last_text)
        if len(last_text):
            line1.append(LayoutWord(last_word.options, s[0], s[1], last_text))
        line1.append(elps)

        # now add back the right half
        first_word = line[w2]
        first_text = first_word.text[s2 + 1:]
        self.options = first_word.options
        s = self.get_extents(first_text)
        if len(first_text):
            line1.append(LayoutWord(first_word.options, s[0], s[1],
                                    first_text))
        line1.extend(line[w2 + 1:])

        lw = sum([word.lw for word in line1])
        lh = max([word.lh for word in line1]) * line_height
        self.options = old_opts
        if uw < lw:
            self.is_shortened = True
        return (lw + 2 * xpad, lh + 2 * ypad,
                [LayoutLine(0, 0, lw, lh, 1, 0, line1)])
示例#2
0
    def _render_real(self):
        lines = self._cached_lines
        options = None
        for line in lines:
            if len(line.words):  # get opts from first line, first word
                options = line.words[0].options
                break
        if not options:  # there was no text to render
            self._render_begin()
            data = self._render_end()
            assert (data)
            if data is not None and data.width > 1:
                self.texture.blit_data(data)
            return

        render_text = self._render_text
        get_extents = self.get_cached_extents()
        uw, uh = options['text_size']
        xpad, ypad = options['padding_x'], options['padding_y']
        x, y = xpad, ypad  # pos in the texture
        iw, ih = self._internal_size  # the real size of text, not texture
        if uw is not None:
            uww = uw - 2 * xpad  # real width of just text
        w, h = self.size
        sw = options['space_width']
        halign = options['halign']
        valign = options['valign']
        split = re.split
        pat = re.compile('( +)')
        self._render_begin()

        if valign == 'bottom':
            y = h - ih + ypad
        elif valign == 'middle':
            y = int((h - ih) / 2 + ypad)

        for layout_line in lines:  # for plain label each line has only one str
            lw, lh = layout_line.w, layout_line.h
            line = ''
            assert len(layout_line.words) < 2
            if len(layout_line.words):
                last_word = layout_line.words[0]
                line = last_word.text
            x = xpad
            if halign[0] == 'c':  # center
                x = int((w - lw) / 2.)
            elif halign[0] == 'r':  # right
                x = max(0, int(w - lw - xpad))

            # right left justify
            # divide left over space between `spaces`
            # TODO implement a better method of stretching glyphs?
            if (uw is not None and halign[-1] == 'y' and line
                    and not layout_line.is_last_line):
                # number spaces needed to fill, and remainder
                n, rem = divmod(max(uww - lw, 0), sw)
                n = int(n)
                words = None
                if n or rem:
                    # there's no trailing space when justify is selected
                    words = split(pat, line)
                if words is not None and len(words) > 1:
                    space = type(line)(' ')
                    # words: every even index is spaces, just add ltr n spaces
                    for i in range(n):
                        idx = (2 * i + 1) % (len(words) - 1)
                        words[idx] = words[idx] + space
                    if rem:
                        # render the last word at the edge, also add it to line
                        ext = get_extents(words[-1])
                        word = LayoutWord(last_word.options, ext[0], ext[1],
                                          words[-1])
                        layout_line.words.append(word)
                        last_word.lw = uww - ext[0]  # word was stretched
                        render_text(words[-1], x + last_word.lw, y)
                        last_word.text = line = ''.join(words[:-2])
                    else:
                        last_word.lw = uww  # word was stretched
                        last_word.text = line = ''.join(words)
                    layout_line.w = uww  # the line occupies full width

            if len(line):
                layout_line.x = x
                layout_line.y = y
                render_text(line, x, y)
            y += lh

        # get data from provider
        data = self._render_end()
        assert (data)

        # If the text is 1px width, usually, the data is black.
        # Don't blit that kind of data, otherwise, you have a little black bar.
        if data is not None and data.width > 1:
            self.texture.blit_data(data)
示例#3
0
    def _pre_render(self):
        # split markup, words, and lines
        # result: list of word with position and width/height
        # during the first pass, we don't care about h/valign
        self._cached_lines = lines = []
        self._refs = {}
        self._anchors = {}
        clipped = False
        w = h = 0
        uw, uh = self.text_size
        spush = self._push_style
        spop = self._pop_style
        options = self.options
        options['_ref'] = None
        options['_anchor'] = None
        options['script'] = 'normal'
        shorten = options['shorten']
        # if shorten, then don't split lines to fit uw, because it will be
        # flattened later when shortening and broken up lines if broken
        # mid-word will have space mid-word when lines are joined
        uw_temp = None if shorten else uw
        xpad = options['padding_x']
        uhh = (None if uh is not None and options['valign'] != 'top'
               or options['shorten'] else uh)
        options['strip'] = options['strip'] or options['halign'] == 'justify'
        find_base_dir = Label.find_base_direction
        base_dir = options['base_direction']
        self._resolved_base_dir = None
        for item in self.markup:
            if item == '[b]':
                spush('bold')
                options['bold'] = True
                self.resolve_font_name()
            elif item == '[/b]':
                spop('bold')
                self.resolve_font_name()
            elif item == '[i]':
                spush('italic')
                options['italic'] = True
                self.resolve_font_name()
            elif item == '[/i]':
                spop('italic')
                self.resolve_font_name()
            elif item == '[u]':
                spush('underline')
                options['underline'] = True
                self.resolve_font_name()
            elif item == '[/u]':
                spop('underline')
                self.resolve_font_name()
            elif item == '[s]':
                spush('strikethrough')
                options['strikethrough'] = True
                self.resolve_font_name()
            elif item == '[/s]':
                spop('strikethrough')
                self.resolve_font_name()
            elif item[:6] == '[size=':
                item = item[6:-1]
                try:
                    if item[-2:] in ('px', 'pt', 'in', 'cm', 'mm', 'dp', 'sp'):
                        size = dpi2px(item[:-2], item[-2:])
                    else:
                        size = int(item)
                except ValueError:
                    raise
                    size = options['font_size']
                spush('font_size')
                options['font_size'] = size
            elif item == '[/size]':
                spop('font_size')
            elif item[:7] == '[color=':
                color = parse_color(item[7:-1])
                spush('color')
                options['color'] = color
            elif item == '[/color]':
                spop('color')
            elif item[:6] == '[font=':
                fontname = item[6:-1]
                spush('font_name')
                options['font_name'] = fontname
                self.resolve_font_name()
            elif item == '[/font]':
                spop('font_name')
                self.resolve_font_name()
            elif item[:13] == '[font_family=':
                spush('font_family')
                options['font_family'] = item[13:-1]
            elif item == '[/font_family]':
                spop('font_family')
            elif item[:14] == '[font_context=':
                fctx = item[14:-1]
                if not fctx or fctx.lower() == 'none':
                    fctx = None
                spush('font_context')
                options['font_context'] = fctx
            elif item == '[/font_context]':
                spop('font_context')
            elif item[:15] == '[font_features=':
                spush('font_features')
                options['font_features'] = item[15:-1]
            elif item == '[/font_features]':
                spop('font_features')
            elif item[:15] == '[text_language=':
                lang = item[15:-1]
                if not lang or lang.lower() == 'none':
                    lang = None
                spush('text_language')
                options['text_language'] = lang
            elif item == '[/text_language]':
                spop('text_language')
            elif item[:5] == '[sub]':
                spush('font_size')
                spush('script')
                options['font_size'] = options['font_size'] * .5
                options['script'] = 'subscript'
            elif item == '[/sub]':
                spop('font_size')
                spop('script')
            elif item[:5] == '[sup]':
                spush('font_size')
                spush('script')
                options['font_size'] = options['font_size'] * .5
                options['script'] = 'superscript'
            elif item == '[/sup]':
                spop('font_size')
                spop('script')
            elif item[:5] == '[ref=':
                ref = item[5:-1]
                spush('_ref')
                options['_ref'] = ref
            elif item == '[/ref]':
                spop('_ref')
            elif not clipped and item[:8] == '[anchor=':
                options['_anchor'] = item[8:-1]
            elif not clipped:
                item = item.replace('&bl;',
                                    '[').replace('&br;',
                                                 ']').replace('&amp;', '&')
                if not base_dir:
                    base_dir = self._resolved_base_dir = find_base_dir(item)
                opts = copy(options)
                extents = self.get_cached_extents()
                opts['space_width'] = extents(' ')[0]
                w, h, clipped = layout_text(item,
                                            lines, (w, h), (uw_temp, uhh),
                                            opts,
                                            extents,
                                            append_down=True,
                                            complete=False)

        if len(lines):  # remove any trailing spaces from the last line
            old_opts = self.options
            self.options = copy(opts)
            w, h, clipped = layout_text('',
                                        lines, (w, h), (uw_temp, uhh),
                                        self.options,
                                        self.get_cached_extents(),
                                        append_down=True,
                                        complete=True)
            self.options = old_opts

        self.is_shortened = False
        if shorten:
            options['_ref'] = None  # no refs for you!
            options['_anchor'] = None
            w, h, lines = self.shorten_post(lines, w, h)
            self._cached_lines = lines
        # when valign is not top, for markup we layout everything (text_size[1]
        # is temporarily set to None) and after layout cut to size if too tall
        elif uh != uhh and h > uh and len(lines) > 1:
            if options['valign'] == 'bottom':
                i = 0
                while i < len(lines) - 1 and h > uh:
                    h -= lines[i].h
                    i += 1
                del lines[:i]
            else:  # middle
                i = 0
                top = int(h / 2. + uh / 2.)  # remove extra top portion
                while i < len(lines) - 1 and h > top:
                    h -= lines[i].h
                    i += 1
                del lines[:i]
                i = len(lines) - 1  # remove remaining bottom portion
                while i and h > uh:
                    h -= lines[i].h
                    i -= 1
                del lines[i + 1:]

        # now justify the text
        if options['halign'] == 'justify' and uw is not None:
            # XXX: update refs to justified pos
            # when justify, each line should've been stripped already
            split = partial(re.split, re.compile('( +)'))
            uww = uw - 2 * xpad
            chr = type(self.text)
            space = chr(' ')
            empty = chr('')

            for i in range(len(lines)):
                line = lines[i]
                words = line.words
                # if there's nothing to justify, we're done
                if (not line.w or int(uww - line.w) <= 0 or not len(words)
                        or line.is_last_line):
                    continue

                done = False
                parts = [
                    None,
                ] * len(words)  # contains words split by space
                idxs = [
                    None,
                ] * len(words)  # indices of the space in parts
                # break each word into spaces and add spaces until it's full
                # do first round of split in case we don't need to split all
                for w in range(len(words)):
                    word = words[w]
                    sw = word.options['space_width']
                    p = parts[w] = split(word.text)
                    idxs[w] = [
                        v for v in range(len(p)) if p[v].startswith(' ')
                    ]
                    # now we have the indices of the spaces in split list
                    for k in idxs[w]:
                        # try to add single space at each space
                        if line.w + sw > uww:
                            done = True
                            break
                        line.w += sw
                        word.lw += sw
                        p[k] += space
                    if done:
                        break

                # there's not a single space in the line?
                if not any(idxs):
                    continue

                # now keep adding spaces to already split words until done
                while not done:
                    for w in range(len(words)):
                        if not idxs[w]:
                            continue
                        word = words[w]
                        sw = word.options['space_width']
                        p = parts[w]
                        for k in idxs[w]:
                            # try to add single space at each space
                            if line.w + sw > uww:
                                done = True
                                break
                            line.w += sw
                            word.lw += sw
                            p[k] += space
                        if done:
                            break

                # if not completely full, push last words to right edge
                diff = int(uww - line.w)
                if diff > 0:
                    # find the last word that had a space
                    for w in range(len(words) - 1, -1, -1):
                        if not idxs[w]:
                            continue
                        break
                    old_opts = self.options
                    self.options = word.options
                    word = words[w]
                    # split that word into left/right and push right till uww
                    l_text = empty.join(parts[w][:idxs[w][-1]])
                    r_text = empty.join(parts[w][idxs[w][-1]:])
                    left = LayoutWord(word.options,
                                      self.get_extents(l_text)[0], word.lh,
                                      l_text)
                    right = LayoutWord(word.options,
                                       self.get_extents(r_text)[0], word.lh,
                                       r_text)
                    left.lw = max(left.lw, word.lw + diff - right.lw)
                    self.options = old_opts

                    # now put words back together with right/left inserted
                    for k in range(len(words)):
                        if idxs[k]:
                            words[k].text = empty.join(parts[k])
                    words[w] = right
                    words.insert(w, left)
                else:
                    for k in range(len(words)):
                        if idxs[k]:
                            words[k].text = empty.join(parts[k])
                line.w = uww
                w = max(w, uww)

        self._internal_size = w, h
        if uw:
            w = uw
        if uh:
            h = uh
        if h > 1 and w < 2:
            w = 2
        if w < 1:
            w = 1
        if h < 1:
            h = 1
        return int(w), int(h)
示例#4
0
    def render_lines(self, lines, options, render_text, y, size):
        get_extents = self.get_cached_extents()
        uw, uh = options['text_size']
        xpad = options['padding_x']
        if uw is not None:
            uww = uw - 2 * xpad  # real width of just text
        w = size[0]
        sw = options['space_width']
        halign = options['halign']
        split = re.split
        find_base_dir = self.find_base_direction
        cur_base_dir = options['base_direction']

        for layout_line in lines:  # for plain label each line has only one str
            lw, lh = layout_line.w, layout_line.h
            line = ''
            assert len(layout_line.words) < 2
            if len(layout_line.words):
                last_word = layout_line.words[0]
                line = last_word.text
                if not cur_base_dir:
                    cur_base_dir = find_base_dir(line)
            x = xpad
            if halign == 'auto':
                if cur_base_dir and 'rtl' in cur_base_dir:
                    x = max(0, int(w - lw - xpad))  # right-align RTL text
            elif halign == 'center':
                x = int((w - lw) / 2.)
            elif halign == 'right':
                x = max(0, int(w - lw - xpad))

            # right left justify
            # divide left over space between `spaces`
            # TODO implement a better method of stretching glyphs?
            if (uw is not None and halign == 'justify' and line and not
                    layout_line.is_last_line):
                # number spaces needed to fill, and remainder
                n, rem = divmod(max(uww - lw, 0), sw)
                n = int(n)
                words = None
                if n or rem:
                    # there's no trailing space when justify is selected
                    words = split(whitespace_pat, line)
                if words is not None and len(words) > 1:
                    space = type(line)(' ')
                    # words: every even index is spaces, just add ltr n spaces
                    for i in range(n):
                        idx = (2 * i + 1) % (len(words) - 1)
                        words[idx] = words[idx] + space
                    if rem:
                        # render the last word at the edge, also add it to line
                        ext = get_extents(words[-1])
                        word = LayoutWord(last_word.options, ext[0], ext[1],
                                          words[-1])
                        layout_line.words.append(word)
                        last_word.lw = uww - ext[0]  # word was stretched
                        render_text(words[-1], x + last_word.lw, y)
                        last_word.text = line = ''.join(words[:-2])
                    else:
                        last_word.lw = uww  # word was stretched
                        last_word.text = line = ''.join(words)
                    layout_line.w = uww  # the line occupies full width

            if len(line):
                layout_line.x = x
                layout_line.y = y
                render_text(line, x, y)
            y += lh
        return y
示例#5
0
文件: markup.py 项目: jpoyau/kivy
    def shorten_post(self, lines, w, h, margin=2):
        """ Shortens the text to a single line according to the label options.

        This function operates on a text that has already been laid out because
        for markup, parts of text can have different size and options.

        If :attr:`text_size` [0] is None, the lines are returned unchanged.
        Otherwise, the lines are converted to a single line fitting within the
        constrained width, :attr:`text_size` [0].

        :params:

            `lines`: list of `LayoutLine` instances describing the text.
            `w`: int, the width of the text in lines, including padding.
            `h`: int, the height of the text in lines, including padding.
            `margin` int, the additional space left on the sides. This is in
            addition to :attr:`padding_x`.

        :returns:
            3-tuple of (xw, h, lines), where w, and h is similar to the input
            and contains the resulting width / height of the text, including
            padding. lines, is a list containing a single `LayoutLine`, which
            contains the words for the line.
        """

        def n(line, c):
            """ A function similar to text.find, except it's an iterator that
            returns successive occurrences of string c in list line. line is
            not a string, but a list of LayoutWord instances that we walk
            from left to right returning the indices of c in the words as we
            encounter them. Note that the options can be different among the
            words.

            :returns:
                3-tuple: the index of the word in line, the index of the
                occurrence in word, and the extents (width) of the combined
                words until this occurrence, not including the occurrence char.
                If no more are found it returns (-1, -1, total_w) where total_w
                is the full width of all the words.
            """
            total_w = 0
            for w in range(len(line)):
                word = line[w]
                if not word.lw:
                    continue
                f = partial(word.text.find, c)
                i = f()
                while i != -1:
                    self.options = word.options
                    yield w, i, total_w + self.get_extents(word.text[:i])[0]
                    i = f(i + 1)
                self.options = word.options
                total_w += self.get_extents(word.text)[0]
            yield -1, -1, total_w  # this should never be reached, really

        def p(line, c):
            """ Similar to the `n` function, except it returns occurrences of c
            from right to left in the list, line, similar to rfind.
            """
            total_w = 0
            offset = 0 if len(c) else 1
            for w in range(len(line) - 1, -1, -1):
                word = line[w]
                if not word.lw:
                    continue
                f = partial(word.text.rfind, c)
                i = f()
                while i != -1:
                    self.options = word.options
                    yield (w, i, total_w + self.get_extents(word.text[i + 1 :])[0])
                    if i:
                        i = f(0, i - offset)
                    else:
                        if not c:
                            self.options = word.options
                            yield (w, -1, total_w + self.get_extents(word.text)[0])
                        break
                self.options = word.options
                total_w += self.get_extents(word.text)[0]
            yield -1, -1, total_w  # this should never be reached, really

        def n_restricted(line, uw, c):
            """ Similar to the function `n`, except it only returns the first
            occurrence and it's not an iterator. Furthermore, if the first
            occurrence doesn't fit within width uw, it returns the index of
            whatever amount of text will still fit in uw.

            :returns:
                similar to the function `n`, except it's a 4-tuple, with the
                last element a boolean, indicating if we had to clip the text
                to fit in uw (True) or if the whole text until the first
                occurrence fitted in uw (False).
            """
            total_w = 0
            if not len(line):
                return 0, 0, 0
            for w in range(len(line)):
                word = line[w]
                f = partial(word.text.find, c)
                self.options = word.options
                extents = self.get_cached_extents()
                i = f()
                if i != -1:
                    ww = extents(word.text[:i])[0]

                if i != -1 and total_w + ww <= uw:  # found and it fits
                    return w, i, total_w + ww, False
                elif i == -1:
                    ww = extents(word.text)[0]
                    if total_w + ww <= uw:  # wasn't found and all fits
                        total_w += ww
                        continue
                    i = len(word.text)

                # now just find whatever amount of the word does fit
                e = 0
                while e != i and total_w + extents(word.text[:e])[0] <= uw:
                    e += 1
                e = max(0, e - 1)
                return w, e, total_w + extents(word.text[:e])[0], True

            return -1, -1, total_w, False

        def p_restricted(line, uw, c):
            """ Similar to `n_restricted`, except it returns the first
            occurrence starting from the right, like `p`.
            """
            total_w = 0
            if not len(line):
                return 0, 0, 0
            for w in range(len(line) - 1, -1, -1):
                word = line[w]
                f = partial(word.text.rfind, c)
                self.options = word.options
                extents = self.get_cached_extents()
                i = f()
                if i != -1:
                    ww = extents(word.text[i + 1 :])[0]

                if i != -1 and total_w + ww <= uw:  # found and it fits
                    return w, i, total_w + ww, False
                elif i == -1:
                    ww = extents(word.text)[0]
                    if total_w + ww <= uw:  # wasn't found and all fits
                        total_w += ww
                        continue

                # now just find whatever amount of the word does fit
                s = len(word.text) - 1
                while s >= 0 and total_w + extents(word.text[s:])[0] <= uw:
                    s -= 1
                return w, s, total_w + extents(word.text[s + 1 :])[0], True

            return -1, -1, total_w, False

        textwidth = self.get_cached_extents()
        uw = self.text_size[0]
        if uw is None:
            return w, h, lines
        old_opts = copy(self.options)
        uw = max(0, int(uw - old_opts["padding_x"] * 2 - margin))
        chr = type(self.text)
        ssize = textwidth(" ")
        c = old_opts["split_str"]
        line_height = old_opts["line_height"]
        xpad, ypad = old_opts["padding_x"], old_opts["padding_y"]
        dir = old_opts["shorten_from"][0]

        # flatten lines into single line
        line = []
        last_w = 0
        for l in range(len(lines)):
            # concatenate (non-empty) inside lines with a space
            this_line = lines[l]
            if last_w and this_line.w and not this_line.line_wrap:
                line.append(LayoutWord(old_opts, ssize[0], ssize[1], chr(" ")))
            last_w = this_line.w or last_w
            for word in this_line.words:
                if word.lw:
                    line.append(word)

        # if that fits, just return the flattened line
        lw = sum([word.lw for word in line])
        if lw <= uw:
            lh = max([word.lh for word in line] + [0]) * line_height
            return lw + 2 * xpad, lh + 2 * ypad, [LayoutLine(0, 0, lw, lh, 1, 0, line)]

        # find the size of ellipsis that'll fit
        elps_s = textwidth("...")
        if elps_s[0] > uw:  # even ellipsis didn't fit...
            s = textwidth("..")
            if s[0] <= uw:
                return (
                    s[0] + 2 * xpad,
                    s[1] * line_height + 2 * ypad,
                    [LayoutLine(0, 0, s[0], s[1], 1, 0, [LayoutWord(old_opts, s[0], s[1], "..")])],
                )
            else:
                s = textwidth(".")
                return (
                    s[0] + 2 * xpad,
                    s[1] * line_height + 2 * ypad,
                    [LayoutLine(0, 0, s[0], s[1], 1, 0, [LayoutWord(old_opts, s[0], s[1], ".")])],
                )
        elps = LayoutWord(old_opts, elps_s[0], elps_s[1], "...")
        uw -= elps_s[0]

        # now find the first left and right words that fit
        w1, e1, l1, clipped1 = n_restricted(line, uw, c)
        w2, s2, l2, clipped2 = p_restricted(line, uw, c)

        if dir != "l":  # center or right
            line1 = None
            if clipped1 or clipped2 or l1 + l2 > uw:
                # if either was clipped or both don't fit, just take first
                if len(c):
                    self.options = old_opts
                    old_opts["split_str"] = ""
                    res = self.shorten_post(lines, w, h, margin)
                    self.options["split_str"] = c
                    return res
                line1 = line[:w1]
                last_word = line[w1]
                last_text = last_word.text[:e1]
                self.options = last_word.options
                s = self.get_extents(last_text)
                line1.append(LayoutWord(last_word.options, s[0], s[1], last_text))
            elif (w1, e1) == (-1, -1):  # this shouldn't occur
                line1 = line
            if line1:
                line1.append(elps)
                lw = sum([word.lw for word in line1])
                lh = max([word.lh for word in line1]) * line_height
                self.options = old_opts
                return lw + 2 * xpad, lh + 2 * ypad, [LayoutLine(0, 0, lw, lh, 1, 0, line1)]

            # now we know that both the first and last word fit, and that
            # there's at least one instances of the split_str in the line
            if (w1, e1) != (w2, s2):  # more than one split_str
                if dir == "r":
                    f = n(line, c)  # iterator
                    assert next(f)[:-1] == (w1, e1)  # first word should match
                    ww1, ee1, l1 = next(f)
                    while l2 + l1 <= uw:
                        w1, e1 = ww1, ee1
                        ww1, ee1, l1 = next(f)
                        if (w1, e1) == (w2, s2):
                            break
                else:  # center
                    f = n(line, c)  # iterator
                    f_inv = p(line, c)  # iterator
                    assert next(f)[:-1] == (w1, e1)
                    assert next(f_inv)[:-1] == (w2, s2)
                    while True:
                        if l1 <= l2:
                            ww1, ee1, l1 = next(f)  # hypothesize that next fit
                            if l2 + l1 > uw:
                                break
                            w1, e1 = ww1, ee1
                            if (w1, e1) == (w2, s2):
                                break
                        else:
                            ww2, ss2, l2 = next(f_inv)
                            if l2 + l1 > uw:
                                break
                            w2, s2 = ww2, ss2
                            if (w1, e1) == (w2, s2):
                                break
        else:  # left
            line1 = [elps]
            if clipped1 or clipped2 or l1 + l2 > uw:
                # if either was clipped or both don't fit, just take last
                if len(c):
                    self.options = old_opts
                    old_opts["split_str"] = ""
                    res = self.shorten_post(lines, w, h, margin)
                    self.options["split_str"] = c
                    return res
                first_word = line[w2]
                first_text = first_word.text[s2 + 1 :]
                self.options = first_word.options
                s = self.get_extents(first_text)
                line1.append(LayoutWord(first_word.options, s[0], s[1], first_text))
                line1.extend(line[w2 + 1 :])
            elif (w1, e1) == (-1, -1):  # this shouldn't occur
                line1 = line
            if len(line1) != 1:
                lw = sum([word.lw for word in line1])
                lh = max([word.lh for word in line1]) * line_height
                self.options = old_opts
                return lw + 2 * xpad, lh + 2 * ypad, [LayoutLine(0, 0, lw, lh, 1, 0, line1)]

            # now we know that both the first and last word fit, and that
            # there's at least one instances of the split_str in the line
            if (w1, e1) != (w2, s2):  # more than one split_str
                f_inv = p(line, c)  # iterator
                assert next(f_inv)[:-1] == (w2, s2)  # last word should match
                ww2, ss2, l2 = next(f_inv)
                while l2 + l1 <= uw:
                    w2, s2 = ww2, ss2
                    ww2, ss2, l2 = next(f_inv)
                    if (w1, e1) == (w2, s2):
                        break

        # now add back the left half
        line1 = line[:w1]
        last_word = line[w1]
        last_text = last_word.text[:e1]
        self.options = last_word.options
        s = self.get_extents(last_text)
        if len(last_text):
            line1.append(LayoutWord(last_word.options, s[0], s[1], last_text))
        elps.options = last_word.options
        line1.append(elps)

        # now add back the right half
        first_word = line[w2]
        first_text = first_word.text[s2 + 1 :]
        self.options = first_word.options
        s = self.get_extents(first_text)
        if len(first_text):
            line1.append(LayoutWord(first_word.options, s[0], s[1], first_text))
        line1.extend(line[w2 + 1 :])

        lw = sum([word.lw for word in line1])
        lh = max([word.lh for word in line1]) * line_height
        self.options = old_opts
        return lw + 2 * xpad, lh + 2 * ypad, [LayoutLine(0, 0, lw, lh, 1, 0, line1)]
示例#6
0
文件: markup.py 项目: jpoyau/kivy
    def _pre_render(self):
        # split markup, words, and lines
        # result: list of word with position and width/height
        # during the first pass, we don't care about h/valign
        self._cached_lines = lines = []
        self._refs = {}
        self._anchors = {}
        clipped = False
        w = h = 0
        uw, uh = self.text_size
        spush = self._push_style
        spop = self._pop_style
        opts = options = self.options
        options["_ref"] = None
        options["_anchor"] = None
        options["script"] = "normal"
        shorten = options["shorten"]
        # if shorten, then don't split lines to fit uw, because it will be
        # flattened later when shortening and broken up lines if broken
        # mid-word will have space mid-word when lines are joined
        uw_temp = None if shorten else uw
        xpad = options["padding_x"]
        uhh = None if uh is not None and options["valign"][-1] != "p" or options["shorten"] else uh
        options["strip"] = options["strip"] or options["halign"][-1] == "y"
        for item in self.markup:
            if item == "[b]":
                spush("bold")
                options["bold"] = True
                self.resolve_font_name()
            elif item == "[/b]":
                spop("bold")
                self.resolve_font_name()
            elif item == "[i]":
                spush("italic")
                options["italic"] = True
                self.resolve_font_name()
            elif item == "[/i]":
                spop("italic")
                self.resolve_font_name()
            elif item[:6] == "[size=":
                item = item[6:-1]
                try:
                    if item[-2:] in ("px", "pt", "in", "cm", "mm", "dp", "sp"):
                        size = dpi2px(item[:-2], item[-2:])
                    else:
                        size = int(item)
                except ValueError:
                    raise
                    size = options["font_size"]
                spush("font_size")
                options["font_size"] = size
            elif item == "[/size]":
                spop("font_size")
            elif item[:7] == "[color=":
                color = parse_color(item[7:-1])
                spush("color")
                options["color"] = color
            elif item == "[/color]":
                spop("color")
            elif item[:6] == "[font=":
                fontname = item[6:-1]
                spush("font_name")
                options["font_name"] = fontname
                self.resolve_font_name()
            elif item == "[/font]":
                spop("font_name")
                self.resolve_font_name()
            elif item[:5] == "[sub]":
                spush("font_size")
                spush("script")
                options["font_size"] = options["font_size"] * 0.5
                options["script"] = "subscript"
            elif item == "[/sub]":
                spop("font_size")
                spop("script")
            elif item[:5] == "[sup]":
                spush("font_size")
                spush("script")
                options["font_size"] = options["font_size"] * 0.5
                options["script"] = "superscript"
            elif item == "[/sup]":
                spop("font_size")
                spop("script")
            elif item[:5] == "[ref=":
                ref = item[5:-1]
                spush("_ref")
                options["_ref"] = ref
            elif item == "[/ref]":
                spop("_ref")
            elif not clipped and item[:8] == "[anchor=":
                options["_anchor"] = item[8:-1]
            elif not clipped:
                item = item.replace("&bl;", "[").replace("&br;", "]").replace("&amp;", "&")
                opts = copy(options)
                extents = self.get_cached_extents()
                opts["space_width"] = extents(" ")[0]
                w, h, clipped = layout_text(item, lines, (w, h), (uw_temp, uhh), opts, extents, True, False)

        if len(lines):  # remove any trailing spaces from the last line
            old_opts = self.options
            self.options = copy(opts)
            w, h, clipped = layout_text(
                "", lines, (w, h), (uw_temp, uhh), self.options, self.get_cached_extents(), True, True
            )
            self.options = old_opts

        if shorten:
            options["_ref"] = None  # no refs for you!
            options["_anchor"] = None
            w, h, lines = self.shorten_post(lines, w, h)
            self._cached_lines = lines
        # when valign is not top, for markup we layout everything (text_size[1]
        # is temporarily set to None) and after layout cut to size if too tall
        elif uh != uhh and h > uh and len(lines) > 1:
            if options["valign"][-1] == "m":  # bottom
                i = 0
                while i < len(lines) - 1 and h > uh:
                    h -= lines[i].h
                    i += 1
                del lines[:i]
            else:  # middle
                i = 0
                top = int(h / 2.0 + uh / 2.0)  # remove extra top portion
                while i < len(lines) - 1 and h > top:
                    h -= lines[i].h
                    i += 1
                del lines[:i]
                i = len(lines) - 1  # remove remaining bottom portion
                while i and h > uh:
                    h -= lines[i].h
                    i -= 1
                del lines[i + 1 :]

        # now justify the text
        if options["halign"][-1] == "y" and uw is not None:
            # XXX: update refs to justified pos
            # when justify, each line shouldv'e been stripped already
            split = partial(re.split, re.compile("( +)"))
            uww = uw - 2 * xpad
            chr = type(self.text)
            space = chr(" ")
            empty = chr("")

            for i in range(len(lines)):
                line = lines[i]
                words = line.words
                # if there's nothing to justify, we're done
                if not line.w or int(uww - line.w) <= 0 or not len(words) or line.is_last_line:
                    continue

                done = False
                parts = [None] * len(words)  # contains words split by space
                idxs = [None] * len(words)  # indices of the space in parts
                # break each word into spaces and add spaces until it's full
                # do first round of split in case we don't need to split all
                for w in range(len(words)):
                    word = words[w]
                    sw = word.options["space_width"]
                    p = parts[w] = split(word.text)
                    idxs[w] = [v for v in range(len(p)) if p[v].startswith(" ")]
                    # now we have the indices of the spaces in split list
                    for k in idxs[w]:
                        # try to add single space at each space
                        if line.w + sw > uww:
                            done = True
                            break
                        line.w += sw
                        word.lw += sw
                        p[k] += space
                    if done:
                        break

                # there's not a single space in the line?
                if not any(idxs):
                    continue

                # now keep adding spaces to already split words until done
                while not done:
                    for w in range(len(words)):
                        if not idxs[w]:
                            continue
                        word = words[w]
                        sw = word.options["space_width"]
                        p = parts[w]
                        for k in idxs[w]:
                            # try to add single space at each space
                            if line.w + sw > uww:
                                done = True
                                break
                            line.w += sw
                            word.lw += sw
                            p[k] += space
                        if done:
                            break

                # if not completely full, push last words to right edge
                diff = int(uww - line.w)
                if diff > 0:
                    # find the last word that had a space
                    for w in range(len(words) - 1, -1, -1):
                        if not idxs[w]:
                            continue
                        break
                    old_opts = self.options
                    self.options = word.options
                    word = words[w]
                    # split that word into left/right and push right till uww
                    l_text = empty.join(parts[w][: idxs[w][-1]])
                    r_text = empty.join(parts[w][idxs[w][-1] :])
                    left = LayoutWord(word.options, self.get_extents(l_text)[0], word.lh, l_text)
                    right = LayoutWord(word.options, self.get_extents(r_text)[0], word.lh, r_text)
                    left.lw = max(left.lw, word.lw + diff - right.lw)
                    self.options = old_opts

                    # now put words back together with right/left inserted
                    for k in range(len(words)):
                        if idxs[k]:
                            words[k].text = empty.join(parts[k])
                    words[w] = right
                    words.insert(w, left)
                else:
                    for k in range(len(words)):
                        if idxs[k]:
                            words[k].text = empty.join(parts[k])
                line.w = uww
                w = max(w, uww)

        self._internal_size = w, h
        if uw:
            w = uw
        if uh:
            h = uh
        if h > 1 and w < 2:
            w = 2
        if w < 1:
            w = 1
        if h < 1:
            h = 1
        return int(w), int(h)
示例#7
0
文件: markup.py 项目: AWolf81/kivy
    def _pre_render(self):
        # split markup, words, and lines
        # result: list of word with position and width/height
        # during the first pass, we don't care about h/valign
        self._cached_lines = lines = []
        self._refs = {}
        self._anchors = {}
        clipped = False
        w = h = 0
        uw, uh = self.text_size
        spush = self._push_style
        spop = self._pop_style
        opts = options = self.options
        options['_ref'] = None
        options['script'] = 'normal'
        shorten = options['shorten']
        # if shorten, then don't split lines to fit uw, because it will be
        # flattened later when shortening and broken up lines if broken
        # mid-word will have space mid-word when lines are joined
        uw_temp = None if shorten else uw
        xpad = options['padding_x']
        uhh = (None if uh is not None and options['valign'][-1] != 'p' or
               options['shorten'] else uh)
        options['strip'] = options['strip'] or options['halign'][-1] == 'y'
        for item in self.markup:
            if item == '[b]':
                spush('bold')
                options['bold'] = True
                self.resolve_font_name()
            elif item == '[/b]':
                spop('bold')
                self.resolve_font_name()
            elif item == '[i]':
                spush('italic')
                options['italic'] = True
                self.resolve_font_name()
            elif item == '[/i]':
                spop('italic')
                self.resolve_font_name()
            elif item[:6] == '[size=':
                item = item[6:-1]
                try:
                    if item[-2:] in ('px', 'pt', 'in', 'cm', 'mm', 'dp', 'sp'):
                        size = dpi2px(item[:-2], item[-2:])
                    else:
                        size = int(item)
                except ValueError:
                    raise
                    size = options['font_size']
                spush('font_size')
                options['font_size'] = size
            elif item == '[/size]':
                spop('font_size')
            elif item[:7] == '[color=':
                color = parse_color(item[7:-1])
                spush('color')
                options['color'] = color
            elif item == '[/color]':
                spop('color')
            elif item[:6] == '[font=':
                fontname = item[6:-1]
                spush('font_name')
                options['font_name'] = fontname
                self.resolve_font_name()
            elif item == '[/font]':
                spop('font_name')
                self.resolve_font_name()
            elif item[:5] == '[sub]':
                spush('font_size')
                spush('script')
                options['font_size'] = options['font_size'] * .5
                options['script'] = 'subscript'
            elif item == '[/sub]':
                spop('font_size')
                spop('script')
            elif item[:5] == '[sup]':
                spush('font_size')
                spush('script')
                options['font_size'] = options['font_size'] * .5
                options['script'] = 'superscript'
            elif item == '[/sup]':
                spop('font_size')
                spop('script')
            elif item[:5] == '[ref=':
                ref = item[5:-1]
                spush('_ref')
                options['_ref'] = ref
            elif item == '[/ref]':
                spop('_ref')
            elif not clipped and item[:8] == '[anchor=':
                ref = item[8:-1]
                if len(lines):
                    x, y = lines[-1].x, lines[-1].y
                else:
                    x = y = 0
                self._anchors[ref] = x, y
            elif not clipped:
                item = item.replace('&bl;', '[').replace(
                    '&br;', ']').replace('&amp;', '&')
                opts = copy(options)
                extents = self.get_cached_extents()
                opts['space_width'] = extents(' ')[0]
                w, h, clipped = layout_text(item, lines, (w, h),
                    (uw_temp, uhh), opts, extents, True, False)

        if len(lines):  # remove any trailing spaces from the last line
            old_opts = self.options
            self.options = copy(opts)
            w, h, clipped = layout_text('', lines, (w, h), (uw_temp, uhh),
                self.options, self.get_cached_extents(), True, True)
            self.options = old_opts

        if shorten:
            options['_ref'] = None  # no refs for you!
            w, h, lines = self.shorten_post(lines, w, h)
            self._cached_lines = lines
        # when valign is not top, for markup we layout everything (text_size[1]
        # is temporarily set to None) and after layout cut to size if too tall
        elif uh != uhh and h > uh and len(lines) > 1:
            if options['valign'][-1] == 'm':  # bottom
                i = 0
                while i < len(lines) - 1 and h > uh:
                    h -= lines[i].h
                    i += 1
                del lines[:i]
            else:  # middle
                i = 0
                top = int(h / 2. + uh / 2.)  # remove extra top portion
                while i < len(lines) - 1 and h > top:
                    h -= lines[i].h
                    i += 1
                del lines[:i]
                i = len(lines) - 1  # remove remaining bottom portion
                while i and h > uh:
                    h -= lines[i].h
                    i -= 1
                del lines[i + 1:]

        # now justify the text
        if options['halign'][-1] == 'y' and uw is not None:
            # XXX: update refs to justified pos
            # when justify, each line shouldv'e been stripped already
            split = partial(re.split, re.compile('( +)'))
            uww = uw - 2 * xpad
            chr = type(self.text)
            space = chr(' ')
            empty = chr('')

            for i in range(len(lines)):
                line = lines[i]
                words = line.words
                # if there's nothing to justify, we're done
                if (not line.w or int(uww - line.w) <= 0 or not len(words) or
                    line.is_last_line):
                    continue

                done = False
                parts = [None, ] * len(words)  # contains words split by space
                idxs = [None, ] * len(words)  # indices of the space in parts
                # break each word into spaces and add spaces until it's full
                # do first round of split in case we don't need to split all
                for w in range(len(words)):
                    word = words[w]
                    sw = word.options['space_width']
                    p = parts[w] = split(word.text)
                    idxs[w] = [v for v in range(len(p)) if
                               p[v].startswith(' ')]
                    # now we have the indices of the spaces in split list
                    for k in idxs[w]:
                        # try to add single space at each space
                        if line.w + sw > uww:
                            done = True
                            break
                        line.w += sw
                        word.lw += sw
                        p[k] += space
                    if done:
                        break

                # there's not a single space in the line?
                if not any(idxs):
                    continue

                # now keep adding spaces to already split words until done
                while not done:
                    for w in range(len(words)):
                        if not idxs[w]:
                            continue
                        word = words[w]
                        sw = word.options['space_width']
                        p = parts[w]
                        for k in idxs[w]:
                            # try to add single space at each space
                            if line.w + sw > uww:
                                done = True
                                break
                            line.w += sw
                            word.lw += sw
                            p[k] += space
                        if done:
                            break

                # if not completely full, push last words to right edge
                diff = int(uww - line.w)
                if diff > 0:
                    # find the last word that had a space
                    for w in range(len(words) - 1, -1, -1):
                        if not idxs[w]:
                            continue
                        break
                    old_opts = self.options
                    self.options = word.options
                    word = words[w]
                    # split that word into left/right and push right till uww
                    l_text = empty.join(parts[w][:idxs[w][-1]])
                    r_text = empty.join(parts[w][idxs[w][-1]:])
                    left = LayoutWord(word.options,
                        self.get_extents(l_text)[0], word.lh, l_text)
                    right = LayoutWord(word.options,
                        self.get_extents(r_text)[0], word.lh, r_text)
                    left.lw = max(left.lw, word.lw + diff - right.lw)
                    self.options = old_opts

                    # now put words back together with right/left inserted
                    for k in range(len(words)):
                        if idxs[k]:
                            words[k].text = empty.join(parts[k])
                    words[w] = right
                    words.insert(w, left)
                else:
                    for k in range(len(words)):
                        if idxs[k]:
                            words[k].text = empty.join(parts[k])
                line.w = uww
                w = max(w, uww)

        self._internal_size = w, h
        if uw:
            w = uw
        if uh:
            h = uh
        if h > 1 and w < 2:
            w = 2
        if w < 1:
            w = 1
        if h < 1:
            h = 1
        return w, h