예제 #1
0
    def composite_tile(self,
                       dst,
                       dst_has_alpha,
                       tx,
                       ty,
                       mipmap_level=0,
                       opacity=1.0,
                       mode=DEFAULT_COMBINE_MODE):
        """Composite one tile of this surface over a NumPy array.

        Composite one tile of this surface over the array dst, modifying only dst.
        """
        if self.mipmap_level < mipmap_level:
            return self.mipmap.composite_tile(dst, dst_has_alpha, tx, ty,
                                              mipmap_level, opacity, mode)

        # Optimization: for some compositing modes, e.g. source-over, an empty
        # source tile leaves the backdrop unchanged.
        if self._SKIP_COMPOSITE_IF_EMPTY[mode]:
            if (tx, ty) not in self.tiledict:
                return
            if opacity == 0:
                return

        with self.tile_request(tx, ty, readonly=True) as src:
            mypaintlib.tile_combine(mode, src, dst, dst_has_alpha, opacity)
예제 #2
0
    def composite_tile(self, dst, dst_has_alpha, tx, ty, mipmap_level=0,
                       opacity=1.0, mode=mypaintlib.CombineNormal,
                       *args, **kwargs):
        """Composite one tile of this surface over a NumPy array.

        See lib.surface.TileCompositable for the parameters. This
        implementation adds two further ones:

        :param float opacity: opacity multiplier
        :param int mode: mode to use when compositing

        """

        if opacity == 0:
            if mode == mypaintlib.CombineDestinationIn or mode == mypaintlib.CombineDestinationAtop:
                if dst_has_alpha:
                    mypaintlib.tile_clear_rgba16(dst)
                    return
            else:
                return

        if self.mipmap_level < mipmap_level:
            self.mipmap.composite_tile(dst, dst_has_alpha, tx, ty,
                                       mipmap_level, opacity, mode)
            return

        with self.tile_request(tx, ty, readonly=True) as src:
            if src is transparent_tile.rgba:
                if mode == mypaintlib.CombineDestinationIn or mode == mypaintlib.CombineDestinationAtop:
                    if dst_has_alpha:
                        mypaintlib.tile_clear_rgba16(dst)
                        return
                else:
                    return
            mypaintlib.tile_combine(mode, src, dst, dst_has_alpha, opacity)
예제 #3
0
    def composite_tile(self,
                       dst,
                       dst_has_alpha,
                       tx,
                       ty,
                       mipmap_level=0,
                       opacity=1.0,
                       mode=mypaintlib.CombineNormal,
                       *args,
                       **kwargs):
        """Composite one tile of this surface over a NumPy array.

        See lib.surface.TileCompositable for the parameters. This
        implementation adds two further ones:

        :param float opacity: opacity multiplier
        :param int mode: mode to use when compositing

        """

        # Apply zero-alpha-source optimizations if possible.
        # Sometimes this can be done without issuing a tile request.
        if opacity == 0:
            if dst_has_alpha:
                if mode in lib.modes.MODES_CLEARING_BACKDROP_AT_ZERO_ALPHA:
                    mypaintlib.tile_clear_rgba16(dst)
                    return
            if mode not in lib.modes.MODES_EFFECTIVE_AT_ZERO_ALPHA:
                return

        # Tile request needed, but may need to satisfy it from a deeper
        # mipmap level.
        if self.mipmap_level < mipmap_level:
            self.mipmap.composite_tile(dst, dst_has_alpha, tx, ty,
                                       mipmap_level, opacity, mode)
            return

        # Tile request at the required level.
        # Try optimizations again if we got the special marker tile
        with self.tile_request(tx, ty, readonly=True) as src:
            if src is transparent_tile.rgba:
                if dst_has_alpha:
                    if mode in lib.modes.MODES_CLEARING_BACKDROP_AT_ZERO_ALPHA:
                        mypaintlib.tile_clear_rgba16(dst)
                        return
                if mode not in lib.modes.MODES_EFFECTIVE_AT_ZERO_ALPHA:
                    return
            mypaintlib.tile_combine(mode, src, dst, dst_has_alpha, opacity)
예제 #4
0
    def composite_tile(self,
                       dst,
                       dst_has_alpha,
                       tx,
                       ty,
                       mipmap_level=0,
                       opacity=1.0,
                       mode=mypaintlib.CombineNormal):
        """Composite one tile of this surface over a NumPy array.

        :param dst: target tile array (uint16, NxNx4, 15-bit scaled int)
        :param dst_has_alpha: alpha channel in dst should be preserved
        :param tx: tile X coordinate, in model tile space
        :param ty: tile Y coordinate, in model tile space
        :param mipmap_level: layer mipmap level to use
        :param opacity: opacity multiplier
        :param mode: mode to use when compositing

        Composite one tile of this surface over the array dst,
        modifying only dst.
        """

        if opacity == 0:
            if mode == mypaintlib.CombineDestinationIn:
                if dst_has_alpha:
                    mypaintlib.tile_clear_rgba16(dst)
                    return
            else:
                return

        if self.mipmap_level < mipmap_level:
            self.mipmap.composite_tile(dst, dst_has_alpha, tx, ty,
                                       mipmap_level, opacity, mode)
            return

        with self.tile_request(tx, ty, readonly=True) as src:
            if src is transparent_tile.rgba:
                if mode == mypaintlib.CombineDestinationIn:
                    if dst_has_alpha:
                        mypaintlib.tile_clear_rgba16(dst)
                        return
                else:
                    return
            mypaintlib.tile_combine(mode, src, dst, dst_has_alpha, opacity)
예제 #5
0
    def composite_tile(self, dst, dst_has_alpha, tx, ty, mipmap_level=0,
                       opacity=1.0, mode=DEFAULT_COMBINE_MODE):
        """Composite one tile of this surface over a NumPy array.

        Composite one tile of this surface over the array dst, modifying only dst.
        """
        if self.mipmap_level < mipmap_level:
            return self.mipmap.composite_tile(dst, dst_has_alpha, tx, ty,
                                              mipmap_level, opacity, mode)

        # Optimization: for some compositing modes, e.g. source-over, an empty
        # source tile leaves the backdrop unchanged.
        if self._SKIP_COMPOSITE_IF_EMPTY[mode]:
            if (tx, ty) not in self.tiledict:
                return
            if opacity == 0:
                return

        with self.tile_request(tx, ty, readonly=True) as src:
            mypaintlib.tile_combine(mode, src, dst, dst_has_alpha, opacity)
예제 #6
0
    def composite_tile(self, dst, dst_has_alpha, tx, ty, mipmap_level=0,
                       opacity=1.0, mode=mypaintlib.CombineNormal,
                       *args, **kwargs):
        """Composite one tile of this surface over a NumPy array.

        See lib.surface.TileCompositable for the parameters. This
        implementation adds two further ones:

        :param float opacity: opacity multiplier
        :param int mode: mode to use when compositing

        """

        # Apply zero-alpha-source optimizations if possible.
        # Sometimes this can be done without issuing a tile request.
        if opacity == 0:
            if dst_has_alpha:
                if mode in lib.modes.MODES_CLEARING_BACKDROP_AT_ZERO_ALPHA:
                    mypaintlib.tile_clear_rgba16(dst)
                    return
            if mode not in lib.modes.MODES_EFFECTIVE_AT_ZERO_ALPHA:
                return

        # Tile request needed, but may need to satisfy it from a deeper
        # mipmap level.
        if self.mipmap_level < mipmap_level:
            self.mipmap.composite_tile(dst, dst_has_alpha, tx, ty,
                                       mipmap_level, opacity, mode)
            return

        # Tile request at the required level.
        # Try optimizations again if we got the special marker tile
        with self.tile_request(tx, ty, readonly=True) as src:
            if src is transparent_tile.rgba:
                if dst_has_alpha:
                    if mode in lib.modes.MODES_CLEARING_BACKDROP_AT_ZERO_ALPHA:
                        mypaintlib.tile_clear_rgba16(dst)
                        return
                if mode not in lib.modes.MODES_EFFECTIVE_AT_ZERO_ALPHA:
                    return
            mypaintlib.tile_combine(mode, src, dst, dst_has_alpha, opacity)
예제 #7
0
    def composite_tile(self, dst, dst_has_alpha, tx, ty, mipmap_level=0,
                       opacity=1.0, mode=mypaintlib.CombineNormal):
        """Composite one tile of this surface over a NumPy array.

        :param dst: target tile array (uint16, NxNx4, 15-bit scaled int)
        :param dst_has_alpha: alpha channel in dst should be preserved
        :param tx: tile X coordinate, in model tile space
        :param ty: tile Y coordinate, in model tile space
        :param mipmap_level: layer mipmap level to use
        :param opacity: opacity multiplier
        :param mode: mode to use when compositing

        Composite one tile of this surface over the array dst,
        modifying only dst.
        """

        if opacity == 0:
            if mode == mypaintlib.CombineDestinationIn:
                if dst_has_alpha:
                    mypaintlib.tile_clear_rgba16(dst)
                    return
            else:
                return

        if self.mipmap_level < mipmap_level:
            self.mipmap.composite_tile(dst, dst_has_alpha, tx, ty,
                                       mipmap_level, opacity, mode)
            return

        with self.tile_request(tx, ty, readonly=True) as src:
            if src is transparent_tile.rgba:
                if mode == mypaintlib.CombineDestinationIn:
                    if dst_has_alpha:
                        mypaintlib.tile_clear_rgba16(dst)
                        return
                else:
                    return
            mypaintlib.tile_combine(mode, src, dst, dst_has_alpha, opacity)
예제 #8
0
def flood_fill(src, x, y, color, bbox, tolerance, dst):
    """Fills connected areas of one surface into another

    :param src: Source surface-like object
    :type src: Anything supporting readonly tile_request()
    :param x: Starting point X coordinate
    :param y: Starting point Y coordinate
    :param color: an RGB color
    :type color: tuple
    :param bbox: Bounding box: limits the fill
    :type bbox: lib.helpers.Rect or equivalent 4-tuple
    :param tolerance: how much filled pixels are permitted to vary
    :type tolerance: float [0.0, 1.0]
    :param dst: Target surface
    :type dst: lib.tiledsurface.MyPaintSurface

    See also `lib.layer.Layer.flood_fill()`.
    """
    # Color to fill with
    fill_r, fill_g, fill_b = color

    # Limits
    tolerance = helpers.clamp(tolerance, 0.0, 1.0)

    # Maximum area to fill: tile and in-tile pixel extents
    bbx, bby, bbw, bbh = bbox
    if bbh <= 0 or bbw <= 0:
        return
    bbbrx = bbx + bbw - 1
    bbbry = bby + bbh - 1
    min_tx = int(bbx // N)
    min_ty = int(bby // N)
    max_tx = int(bbbrx // N)
    max_ty = int(bbbry // N)
    min_px = int(bbx % N)
    min_py = int(bby % N)
    max_px = int(bbbrx % N)
    max_py = int(bbbry % N)

    # Tile and pixel addressing for the seed point
    tx, ty = int(x // N), int(y // N)
    px, py = int(x % N), int(y % N)

    # Sample the pixel color there to obtain the target color
    with src.tile_request(tx, ty, readonly=True) as start:
        targ_r, targ_g, targ_b, targ_a = [int(c) for c in start[py][px]]
    if targ_a == 0:
        targ_r = 0
        targ_g = 0
        targ_b = 0
        targ_a = 0

    # Flood-fill loop
    filled = {}
    tileq = [
        ((tx, ty),
         [(px, py)])
    ]
    while len(tileq) > 0:
        (tx, ty), seeds = tileq.pop(0)
        # Bbox-derived limits
        if tx > max_tx or ty > max_ty:
            continue
        if tx < min_tx or ty < min_ty:
            continue
        # Pixel limits within this tile...
        min_x = 0
        min_y = 0
        max_x = N-1
        max_y = N-1
        # ... vary at the edges
        if tx == min_tx:
            min_x = min_px
        if ty == min_ty:
            min_y = min_py
        if tx == max_tx:
            max_x = max_px
        if ty == max_ty:
            max_y = max_py
        # Flood-fill one tile
        with src.tile_request(tx, ty, readonly=True) as src_tile:
            dst_tile = filled.get((tx, ty), None)
            if dst_tile is None:
                dst_tile = numpy.zeros((N, N, 4), 'uint16')
                filled[(tx, ty)] = dst_tile
            overflows = mypaintlib.tile_flood_fill(
                src_tile, dst_tile, seeds,
                targ_r, targ_g, targ_b, targ_a,
                fill_r, fill_g, fill_b,
                min_x, min_y, max_x, max_y,
                tolerance
            )
            seeds_n, seeds_e, seeds_s, seeds_w = overflows
        # Enqueue overflows in each cardinal direction
        if seeds_n and ty > min_ty:
            tpos = (tx, ty-1)
            tileq.append((tpos, seeds_n))
        if seeds_w and tx > min_tx:
            tpos = (tx-1, ty)
            tileq.append((tpos, seeds_w))
        if seeds_s and ty < max_ty:
            tpos = (tx, ty+1)
            tileq.append((tpos, seeds_s))
        if seeds_e and tx < max_tx:
            tpos = (tx+1, ty)
            tileq.append((tpos, seeds_e))

    # Composite filled tiles into the destination surface
    mode = mypaintlib.CombineNormal
    for (tx, ty), src_tile in filled.iteritems():
        with dst.tile_request(tx, ty, readonly=False) as dst_tile:
            mypaintlib.tile_combine(mode, src_tile, dst_tile, True, 1.0)
        dst._mark_mipmap_dirty(tx, ty)
    bbox = lib.surface.get_tiles_bbox(filled)
    dst.notify_observers(*bbox)
예제 #9
0
def flood_fill(src, x, y, color, bbox, tolerance, dst):
    """Fills connected areas of one surface into another

    :param src: Source surface-like object
    :type src: Anything supporting readonly tile_request()
    :param x: Starting point X coordinate
    :param y: Starting point Y coordinate
    :param color: an RGB color
    :type color: tuple
    :param bbox: Bounding box: limits the fill
    :type bbox: lib.helpers.Rect or equivalent 4-tuple
    :param tolerance: how much filled pixels are permitted to vary
    :type tolerance: float [0.0, 1.0]
    :param dst: Target surface
    :type dst: lib.tiledsurface.MyPaintSurface

    See also `lib.layer.Layer.flood_fill()`.
    """
    # Color to fill with
    fill_r, fill_g, fill_b = color

    # Limits
    tolerance = helpers.clamp(tolerance, 0.0, 1.0)

    # Maximum area to fill: tile and in-tile pixel extents
    bbx, bby, bbw, bbh = bbox
    if bbh <= 0 or bbw <= 0:
        return
    bbbrx = bbx + bbw - 1
    bbbry = bby + bbh - 1
    min_tx = int(bbx // N)
    min_ty = int(bby // N)
    max_tx = int(bbbrx // N)
    max_ty = int(bbbry // N)
    min_px = int(bbx % N)
    min_py = int(bby % N)
    max_px = int(bbbrx % N)
    max_py = int(bbbry % N)

    # Tile and pixel addressing for the seed point
    tx, ty = int(x // N), int(y // N)
    px, py = int(x % N), int(y % N)

    # Sample the pixel color there to obtain the target color
    with src.tile_request(tx, ty, readonly=True) as start:
        targ_r, targ_g, targ_b, targ_a = [int(c) for c in start[py][px]]
    if targ_a == 0:
        targ_r = 0
        targ_g = 0
        targ_b = 0
        targ_a = 0

    # Flood-fill loop
    filled = {}
    tileq = [((tx, ty), [(px, py)])]
    while len(tileq) > 0:
        (tx, ty), seeds = tileq.pop(0)
        # Bbox-derived limits
        if tx > max_tx or ty > max_ty:
            continue
        if tx < min_tx or ty < min_ty:
            continue
        # Pixel limits within this tile...
        min_x = 0
        min_y = 0
        max_x = N - 1
        max_y = N - 1
        # ... vary at the edges
        if tx == min_tx:
            min_x = min_px
        if ty == min_ty:
            min_y = min_py
        if tx == max_tx:
            max_x = max_px
        if ty == max_ty:
            max_y = max_py
        # Flood-fill one tile
        with src.tile_request(tx, ty, readonly=True) as src_tile:
            dst_tile = filled.get((tx, ty), None)
            if dst_tile is None:
                dst_tile = numpy.zeros((N, N, 4), 'uint16')
                filled[(tx, ty)] = dst_tile
            overflows = mypaintlib.tile_flood_fill(src_tile, dst_tile, seeds,
                                                   targ_r, targ_g, targ_b,
                                                   targ_a, fill_r, fill_g,
                                                   fill_b, min_x, min_y, max_x,
                                                   max_y, tolerance)
            seeds_n, seeds_e, seeds_s, seeds_w = overflows
        # Enqueue overflows in each cardinal direction
        if seeds_n and ty > min_ty:
            tpos = (tx, ty - 1)
            tileq.append((tpos, seeds_n))
        if seeds_w and tx > min_tx:
            tpos = (tx - 1, ty)
            tileq.append((tpos, seeds_w))
        if seeds_s and ty < max_ty:
            tpos = (tx, ty + 1)
            tileq.append((tpos, seeds_s))
        if seeds_e and tx < max_tx:
            tpos = (tx + 1, ty)
            tileq.append((tpos, seeds_e))

    # Composite filled tiles into the destination surface
    mode = mypaintlib.CombineNormal
    for (tx, ty), src_tile in filled.iteritems():
        with dst.tile_request(tx, ty, readonly=False) as dst_tile:
            mypaintlib.tile_combine(mode, src_tile, dst_tile, True, 1.0)
        dst._mark_mipmap_dirty(tx, ty)
    bbox = lib.surface.get_tiles_bbox(filled)
    dst.notify_observers(*bbox)
예제 #10
0
    def cairo_request(self, x, y, w, h, mode=lib.modes.DEFAULT_MODE):
        """Get a Cairo context for a given area, then put back changes.

        :param int x: Request area's X coordinate.
        :param int y: Request area's Y coordinate.
        :param int w: Request area's width.
        :param int h: Request area's height.
        :param mode: Combine mode for the put-back.
        :rtype: contextlib.GeneratorContextManager
        :returns: cairo.Context (via with-statement)

        This context manager works by constructing and managing a
        temporary 8bpp Cairo imagesurface and yielding up a Cairo
        context that points at it. Call the method as part of a
        with-statement:

        >>> s = MyPaintSurface()
        >>> n = TILE_SIZE
        >>> assert n > 10
        >>> with s.cairo_request(10, 10, n, n) as cr:
        ...     cr.set_source_rgb(1, 0, 0)
        ...     cr.rectangle(1, 1, n-2, n-2)
        ...     cr.fill()
        >>> list(sorted(s.tiledict.keys()))
        [(0, 0), (0, 1), (1, 0), (1, 1)]

        If the mode is specified, it must be a layer/surface combine
        mode listed in `lib.modes.STACK_MODES`. In this case, The
        temporary cairo ImageSurface object is initially completely
        transparent, and anything you draw to it is composited back over
        the surface using the mode you requested.

        If you pass mode=None (or mode=lib.modes.PASS_THROUGH_MODE),
        Cairo operates directly on the surface. This means that you can
        use Cairo operators and primitives which erase tile data.

        >>> import cairo
        >>> with s.cairo_request(0, 0, n, n, mode=None) as cr:
        ...     cr.set_operator(cairo.OPERATOR_CLEAR)
        ...     cr.set_source_rgb(1, 1, 1)
        ...     cr.paint()
        >>> _ignored = s.remove_empty_tiles()
        >>> list(sorted(s.tiledict.keys()))
        [(0, 1), (1, 0), (1, 1)]
        >>> with s.cairo_request(n-10, n-10, n+10, n+10, mode=None) as cr:
        ...     cr.set_operator(cairo.OPERATOR_SOURCE)
        ...     cr.set_source_rgba(0, 1, 0, 0)
        ...     cr.paint()
        >>> _ignored = s.remove_empty_tiles()
        >>> list(sorted(s.tiledict.keys()))
        [(0, 1), (1, 0)]

        See also:

        * lib.pixbufsurface.Surface.cairo_request()
        * lib.layer.data.SimplePaintingMode.cairo_request()

        """

        # Normalize and validate args
        if mode is not None:
            if mode == lib.modes.PASS_THROUGH_MODE:
                mode = None
            elif mode not in lib.modes.STANDARD_MODES:
                raise ValueError(
                    "The 'mode' argument must be one of STANDARD_MODES, "
                    "or it must be either PASS_THROUGH_MODE or None."
                )
        x = int(x)
        y = int(y)
        w = int(w)
        h = int(h)
        if w <= 0 or h <= 0:
            return   # nothing to do

        # Working pixbuf-surface
        s = lib.pixbufsurface.Surface(x, y, w, h)
        dirty_tiles = list(s.get_tiles())

        # Populate it with an 8-bit downsampling of this surface's data
        # if we're going to be "working directly"
        if mode is None:
            for tx, ty in dirty_tiles:
                with s.tile_request(tx, ty, readonly=False) as dst:
                    self.blit_tile_into(dst, True, tx, ty)

        # Collect changes from the invoker...
        with s.cairo_request() as cr:
            yield cr

        # Blit or composite back the changed pixbuf
        if mode is None:
            for tx, ty in dirty_tiles:
                with self.tile_request(tx, ty, readonly=False) as dst:
                    s.blit_tile_into(dst, True, tx, ty)
        else:
            tmp = np.zeros((N, N, 4), 'uint16')
            for tx, ty in dirty_tiles:
                s.blit_tile_into(tmp, True, tx, ty)
                with self.tile_request(tx, ty, readonly=False) as dst:
                    mypaintlib.tile_combine(mode, tmp, dst, True, 1.0)

        # Tell everyone about the changes
        bbox = lib.surface.get_tiles_bbox(dirty_tiles)
        self.notify_observers(*bbox)
예제 #11
0
    def cairo_request(self, x, y, w, h, mode=lib.modes.DEFAULT_MODE):
        """Get a Cairo context for a given area, then put back changes.

        :param int x: Request area's X coordinate.
        :param int y: Request area's Y coordinate.
        :param int w: Request area's width.
        :param int h: Request area's height.
        :param mode: Combine mode for the put-back.
        :rtype: contextlib.GeneratorContextManager
        :returns: cairo.Context (via with-statement)

        This context manager works by constructing and managing a
        temporary 8bpp Cairo imagesurface and yielding up a Cairo
        context that points at it. Call the method as part of a
        with-statement:

        >>> s = MyPaintSurface()
        >>> n = TILE_SIZE
        >>> assert n > 10
        >>> with s.cairo_request(10, 10, n, n) as cr:
        ...     cr.set_source_rgb(1, 0, 0)
        ...     cr.rectangle(1, 1, n-2, n-2)
        ...     cr.fill()
        >>> list(sorted(s.tiledict.keys()))
        [(0, 0), (0, 1), (1, 0), (1, 1)]

        If the mode is specified, it must be a layer/surface combine
        mode listed in `lib.modes.STACK_MODES`. In this case, The
        temporary cairo ImageSurface object is initially completely
        transparent, and anything you draw to it is composited back over
        the surface using the mode you requested.

        If you pass mode=None (or mode=lib.modes.PASS_THROUGH_MODE),
        Cairo operates directly on the surface. This means that you can
        use Cairo operators and primitives which erase tile data.

        >>> import cairo
        >>> with s.cairo_request(0, 0, n, n, mode=None) as cr:
        ...     cr.set_operator(cairo.OPERATOR_CLEAR)
        ...     cr.set_source_rgb(1, 1, 1)
        ...     cr.paint()
        >>> s.remove_empty_tiles()
        >>> list(sorted(s.tiledict.keys()))
        [(0, 1), (1, 0), (1, 1)]
        >>> with s.cairo_request(n-10, n-10, n+10, n+10, mode=None) as cr:
        ...     cr.set_operator(cairo.OPERATOR_SOURCE)
        ...     cr.set_source_rgba(0, 1, 0, 0)
        ...     cr.paint()
        >>> s.remove_empty_tiles()
        >>> list(sorted(s.tiledict.keys()))
        [(0, 1), (1, 0)]

        See also:

        * lib.pixbufsurface.Surface.cairo_request()
        * lib.layer.data.SimplePaintingMode.cairo_request()

        """

        # Normalize and validate args
        if mode is not None:
            if mode == lib.modes.PASS_THROUGH_MODE:
                mode = None
            elif mode not in lib.modes.STANDARD_MODES:
                raise ValueError(
                    "The 'mode' argument must be one of STANDARD_MODES, "
                    "or it must be either PASS_THROUGH_MODE or None."
                )
        x = int(x)
        y = int(y)
        w = int(w)
        h = int(h)
        if w <= 0 or h <= 0:
            return   # nothing to do

        # Working pixbuf-surface
        s = lib.pixbufsurface.Surface(x, y, w, h)
        dirty_tiles = list(s.get_tiles())

        # Populate it with an 8-bit downsampling of this surface's data
        # if we're going to be "working directly"
        if mode is None:
            for tx, ty in dirty_tiles:
                with s.tile_request(tx, ty, readonly=False) as dst:
                    self.blit_tile_into(dst, True, tx, ty)

        # Collect changes from the invoker...
        with s.cairo_request() as cr:
            yield cr

        # Blit or composite back the changed pixbuf
        if mode is None:
            for tx, ty in dirty_tiles:
                with self.tile_request(tx, ty, readonly=False) as dst:
                    s.blit_tile_into(dst, True, tx, ty)
        else:
            tmp = np.zeros((N, N, 4), 'uint16')
            for tx, ty in dirty_tiles:
                s.blit_tile_into(tmp, True, tx, ty)
                with self.tile_request(tx, ty, readonly=False) as dst:
                    mypaintlib.tile_combine(mode, tmp, dst, True, 1.0)

        # Tell everyone about the changes
        bbox = lib.surface.get_tiles_bbox(dirty_tiles)
        self.notify_observers(*bbox)