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)
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)
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)
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)
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)
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)
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)
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)