Ejemplo n.º 1
0
    def save_to_string(self, translate_x, translate_y):
        """Return a compressed bytes string representing the stroke shape.

        This can be used with init_from_string on subsequent file loads.

        >>> shape = StrokeShape._mock()
        >>> bstr = shape.save_to_string(-N, 2*N)
        >>> isinstance(bstr, bytes)
        True

        See lib.layer.data.PaintingLayer.save_to_openraster().
        Format: "v2" strokemap format.

        """
        assert translate_x % N == 0
        assert translate_y % N == 0
        translate_x = int(translate_x // N)
        translate_y = int(translate_y // N)
        self.tasks.finish_all()
        data = b''
        for (tx, ty), tile in iteritems(self.strokemap):
            compressed_bitmap = tile.to_bytes()
            tx = int(tx + translate_x)
            ty = int(ty + translate_y)
            data += struct.pack('>iiI', tx, ty, len(compressed_bitmap))
            data += compressed_bitmap
        return data
Ejemplo n.º 2
0
def unseep(seed_queue, filled, gc_filler, total_px, tiles_bbox, distances):
    """Seep inversion is basically a four-way 0-alpha fill
    with different conditions. It only backs off into the original
    fill and therefore does not require creation of new tiles or use
    of an input alpha tile.
    """
    backup = {}
    while len(seed_queue) > 0:
        tile_coord, seeds, is_initial = seed_queue.pop(0)
        if tile_coord not in distances or tile_coord not in filled:
            continue
        if tile_coord not in backup:
            if filled[tile_coord] is _FULL_TILE:
                backup[tile_coord] = _FULL_TILE
                filled[tile_coord] = fc.new_full_tile(1 << 15)
            else:
                backup[tile_coord] = np.copy(filled[tile_coord])
        result = gc_filler.unseep(distances[tile_coord], filled[tile_coord],
                                  seeds, is_initial)
        overflows = result[0:4]
        num_erased_pixels = result[4]
        total_px -= num_erased_pixels
        enqueue_overflows(seed_queue, tile_coord, overflows, tiles_bbox,
                          (False, ) * 4)
    if total_px <= 0:
        # For small areas, when starting on a distance-marked pixel,
        # backing off may remove the entire fill, in which case we
        # roll back the tiles that were processed
        for tile_coord, tile in iteritems(backup):
            filled[tile_coord] = tile
Ejemplo n.º 3
0
def scanline_fill(handler, src, seed_lists, tiles_bbox, filler):
    """ Perform a scanline fill and return the filled tiles

    Perform a scanline fill using the given starting point and tile,
    with reference to the src surface and given bounding box, using the
    provided filler instance.

    :param handler: updates fill status and permits cancelling
    :type handler: FillHandler
    :param src: Source surface-like object
    :param seed_lists: dictionary, pairing tile coords with lists of seeds
    :type seed_lists: dict
    :param tiles_bbox: Bounding box for the fill
    :type tiles_bbox: lib.fill_common.TileBoundingBox
    :param filler: filler instance performing the per-tile fill operation
    :type filler: myplib.Filler
    :returns: a dictionary of coord->tile mappings for the filled tiles
    """
    # Dict of coord->tile data populated during the fill
    filled = {}

    inv_edges = (
        EDGE.south,
        EDGE.west,
        EDGE.north,
        EDGE.east
    )

    # Starting coordinates + direction of origin (from within)
    tileq = []
    for seed_tile_coord, seeds in iteritems(seed_lists):
        tileq.append((seed_tile_coord, seeds, myplib.edges.none))

    tfs = _TileFillSkipper(tiles_bbox, filler, set({}))

    while len(tileq) > 0 and handler.run:
        tile_coord, seeds, from_dir = tileq.pop(0)
        # Skip if the tile has been fully processed already
        if tile_coord in tfs.final:
            continue
        # Flood-fill one tile
        with src.tile_request(*tile_coord, readonly=True) as src_tile:
            # See if the tile can be skipped
            overflows = tfs.check(tile_coord, src_tile, filled, from_dir)
            if overflows is None:
                if tile_coord not in filled:
                    handler.inc_processed()
                    filled[tile_coord] = np.zeros((N, N), 'uint16')
                overflows = filler.fill(
                    src_tile, filled[tile_coord], seeds,
                    from_dir, *tiles_bbox.tile_bounds(tile_coord)
                )
            else:
                handler.inc_processed()
        enqueue_overflows(tileq, tile_coord, overflows, tiles_bbox, inv_edges)
    return filled
    def _init_adjustments(self):
        """Initializes adjustments for the scales used internally

        When running as part of the MyPaint app, the brush setting ones are
        shared with it.
        """
        # Brush setting base values
        if self.app:
            for s in brushsettings.settings_visible:
                adj = self.app.brush_adjustment[s.cname]
                self._base_adj[s.cname] = adj
            # The application instance manages value-changed callbacks itself.
        else:
            for s in brushsettings.settings_visible:
                adj = Gtk.Adjustment(value=s.default,
                                     lower=s.min,
                                     upper=s.max,
                                     step_increment=0.01,
                                     page_increment=0.1)
                self._base_adj[s.cname] = adj
            changed_cb = self._testmode_base_value_adj_changed_cb
            for cname, adj in iteritems(self._base_adj):
                adj.connect('value-changed', changed_cb, cname)
        # Per-input scale maxima and minima
        for inp in brushsettings.inputs:
            name = inp.name
            adj = Gtk.Adjustment(value=1.0 / 4.0,
                                 lower=-1.0,
                                 upper=1.0,
                                 step_increment=0.01,
                                 page_increment=0.1)
            adj.connect("value-changed", self.input_adj_changed_cb, inp)
            self._input_y_adj[name] = adj
            lower = -20.0
            upper = +20.0
            # Pre-libmypaint split, the limits were read from json and could be
            # None. Now that cannot be checked directly, so instead check if
            # the limits are extreme (in libmypaint, they are set to +-FLT_MAX)
            if abs(inp.hard_min) < 1e16:
                lower = inp.hard_min
            if abs(inp.hard_max) < 1e16:
                upper = inp.hard_max
            adj = Gtk.Adjustment(value=inp.soft_min,
                                 lower=lower,
                                 upper=upper - 0.1,
                                 step_increment=0.01,
                                 page_increment=0.1)
            adj.connect("value-changed", self.input_adj_changed_cb, inp)
            self._input_xmin_adj[name] = adj
            adj = Gtk.Adjustment(value=inp.soft_max,
                                 lower=lower + 0.1,
                                 upper=upper,
                                 step_increment=0.01,
                                 page_increment=0.1)
            adj.connect("value-changed", self.input_adj_changed_cb, inp)
            self._input_xmax_adj[name] = adj
Ejemplo n.º 5
0
def gap_closing_fill(handler, src, seed_lists, tiles_bbox, filler,
                     gap_closing_options):
    """ Fill loop that finds and uses gap data to avoid unwanted leaks

    Gaps are defined as distances of fillable pixels enclosed on two sides
    by unfillable pixels. Each tile considered, and their neighbours, are
    flooded with alpha values based on the target color and threshold values.
    The resulting alphas are then searched for gaps, and the size of these gaps
    are marked in separate tiles - one for each tile filled.
    """

    unseep_queue = []
    filled = {}
    final = set({})

    seed_queue = []
    for seed_tile_coord, seeds in iteritems(seed_lists):
        seed_queue.append((seed_tile_coord, seeds))

    options = gap_closing_options
    max_gap_size = lib.helpers.clamp(options.max_gap_size, 1, TILE_SIZE)
    gc_filler = myplib.GapClosingFiller(max_gap_size, options.retract_seeps)
    gc_handler = _GCTileHandler(final, max_gap_size, tiles_bbox, filler, src)
    total_px = 0
    skip_unseeping = False

    while len(seed_queue) > 0 and handler.run:
        tile_coord, seeds = seed_queue.pop(0)
        if tile_coord in final:
            continue
        # Create distance-data and alpha output tiles for the fill
        # and check if the tile can be skipped directly
        alpha_t, dist_t, overflows = gc_handler.get_gc_data(tile_coord, seeds)
        if overflows:
            handler.inc_processed()
            filled[tile_coord] = _FULL_TILE
        else:
            # Complement data for initial seeds (if they are initial seeds)
            seeds, any_not_max = complement_gc_seeds(seeds, dist_t)
            # If the fill is starting at a point with a detected distance,
            # disable seep retraction - otherwise it is very likely
            # that the result will be completely empty.
            if any_not_max:
                skip_unseeping = True
            # Pixel limits within tiles can vary at the bounding box edges
            px_bounds = tiles_bbox.tile_bounds(tile_coord)
            # Create new output tile if not already present
            if tile_coord not in filled:
                handler.inc_processed()
                filled[tile_coord] = np.zeros((N, N), 'uint16')
            # Run the gap-closing fill for the tile
            result = gc_filler.fill(alpha_t, dist_t, filled[tile_coord], seeds,
                                    *px_bounds)
            overflows = result[0:4]
            fill_edges, px_f = result[4:6]
            # The entire tile was filled, despite potential gaps;
            # replace data w. constant and mark tile as final.
            if px_f == N * N:
                final.add(tile_coord)
            # When seep inversion is enabled, track total pixels filled
            # and coordinates where the fill stopped due to distance conditions
            total_px += px_f
            if not skip_unseeping and fill_edges:
                unseep_queue.append((tile_coord, fill_edges, True))
        # Enqueue overflows, whether skipping or not
        enqueue_overflows(seed_queue, tile_coord, overflows, tiles_bbox)

    # If enabled, pull the fill back into the gaps to stop before them
    if not skip_unseeping and handler.run:
        unseep(unseep_queue, filled, gc_filler, total_px, tiles_bbox,
               gc_handler.distances)
    return filled
Ejemplo n.º 6
0
def composite(handler, fill_args, trim_result, filled, tiles_bbox, dst):
    """Composite the filled tiles into the destination surface"""

    handler.set_stage(handler.COMPOSITE, len(filled))

    fill_col = fill_args.color

    # Prepare opaque color rgba tile for copying
    full_rgba = myplib.rgba_tile_from_alpha_tile(
        _FULL_TILE, *(fill_col + (0, 0, N - 1, N - 1)))

    # Bounding box of tiles that need updating
    dst_changed_bbox = None
    dst_tiles = dst.get_tiles()

    skip_empty_dst = fill_args.skip_empty_dst()
    mode = fill_args.mode
    lock_alpha = fill_args.lock_alpha
    opacity = fill_args.opacity

    tile_combine = myplib.tile_combine

    # Composite filled tiles into the destination surface
    for tile_coord, src_tile in iteritems(filled):

        if not handler.run:
            break

        handler.inc_processed()

        # Omit tiles outside of the bounding box _if_ the frame is enabled
        # Note:filled tiles outside bbox only originates from dilation/blur
        if trim_result and tiles_bbox.outside(tile_coord):
            continue

        # Skip empty destination tiles for erasing and alpha locking
        # Avoids completely unnecessary tile allocation and copying
        if skip_empty_dst and tile_coord not in dst_tiles:
            continue

        with dst.tile_request(*tile_coord, readonly=False) as dst_tile:

            # Only at this point might the bounding box need to be updated
            dst_changed_bbox = update_bbox(dst_changed_bbox, *tile_coord)

            # Under certain conditions, direct copies and dict manipulation
            # can be used instead of compositing operations.
            cut_off = trim_result and tiles_bbox.crossing(tile_coord)
            full_inner = src_tile is _FULL_TILE and not cut_off
            if full_inner:
                if mode == myplib.CombineNormal and opacity == 1.0:
                    myplib.tile_copy_rgba16_into_rgba16(full_rgba, dst_tile)
                    continue
                elif mode == myplib.CombineDestinationOut and opacity == 1.0:
                    dst_tiles.pop(tile_coord)
                    continue
                elif mode == myplib.CombineDestinationIn and opacity == 1.0:
                    continue
                # Even if opacity != 1.0, we can reuse the full rgba tile
                src_tile_rgba = full_rgba
            else:
                if trim_result:
                    tile_bounds = tiles_bbox.tile_bounds(tile_coord)
                else:
                    tile_bounds = (0, 0, N - 1, N - 1)
                src_tile_rgba = myplib.rgba_tile_from_alpha_tile(
                    src_tile, *(fill_col + tile_bounds))

            # If alpha locking is enabled in combination with a mode other than
            # CombineNormal, we need to copy the dst tile to mask the result
            if lock_alpha and mode != myplib.CombineSourceAtop:
                mask = np.copy(dst_tile)
                mask_mode = myplib.CombineDestinationAtop
                tile_combine(mode, src_tile_rgba, dst_tile, True, opacity)
                tile_combine(mask_mode, mask, dst_tile, True, 1.0)
            else:
                tile_combine(mode, src_tile_rgba, dst_tile, True, opacity)

    # Handle dst-out and dst-atop: clear untouched tiles
    if mode in [myplib.CombineDestinationIn, myplib.CombineDestinationAtop]:
        for tile_coord in list(dst_tiles.keys()):
            if not handler.run:
                break
            if tile_coord not in filled:
                dst_changed_bbox = update_bbox(dst_changed_bbox, *tile_coord)
                with dst.tile_request(*tile_coord, readonly=False):
                    dst_tiles.pop(tile_coord)

    if dst_changed_bbox and handler.run:
        min_tx, min_ty, max_tx, max_ty = dst_changed_bbox
        bbox = (
            min_tx * N,
            min_ty * N,
            (1 + max_tx - min_tx) * N,
            (1 + max_ty - min_ty) * N,
        )
        # Even for large fills on slow machines, this stage
        # will almost always be too short to even notice.
        # It is not cancellable once entered.
        handler.set_stage(FillHandler.FINISHING)

        # The observers may directly or indirectly use the
        # Gtk API, so the call is scheduled on the gui thread.
        GLib.idle_add(dst.notify_observers, *bbox)
    def rename_button_clicked_cb(self, button):
        """Rename the current brush; user is prompted for a new name"""
        bm = self.app.brushmanager
        src_brush = bm.selected_brush
        if not src_brush.name:
            dialogs.error(
                self,
                C_(
                    'brush settings editor: rename brush: error message',
                    'No brush selected!',
                ))
            return

        src_name_pp = src_brush.name.replace('_', ' ')
        dst_name = dialogs.ask_for_name(
            self,
            C_(
                "brush settings editor: rename brush: dialog title",
                "Rename Brush",
            ),
            src_name_pp,
        )
        if not dst_name:
            return
        dst_name = dst_name.replace(' ', '_')
        # ensure we don't overwrite an existing brush by accident
        dst_deleted = None

        for group, brushes in iteritems(bm.groups):
            for b2 in brushes:
                if b2.name == dst_name:
                    if group == brushmanager.DELETED_BRUSH_GROUP:
                        dst_deleted = b2
                    else:
                        msg = C_(
                            'brush settings editor: '
                            'rename brush: error message',
                            'A brush with this name already exists!',
                        )
                        dialogs.error(self, msg)
                        return

        logger.info("Renaming brush %r -> %r", src_brush.name, dst_name)
        if dst_deleted:
            deleted_group = brushmanager.DELETED_BRUSH_GROUP
            deleted_brushes = bm.get_group_brushes(deleted_group)
            deleted_brushes.remove(dst_deleted)
            bm.brushes_changed(deleted_brushes)

        # save src as dst
        src_name = src_brush.name
        src_brush.name = dst_name
        src_brush.save()
        src_brush.name = src_name
        # load dst
        dst_brush = brushmanager.ManagedBrush(bm, dst_name, persistent=True)
        dst_brush.load()

        # Replace src with dst, but keep src in the deleted list if it
        # is a stock brush
        self._delete_brush(src_brush, replacement=dst_brush)

        bm.select_brush(dst_brush)