1# This file is part of MyPaint.
2# -*- encoding: utf-8 -*-
3# Copyright (C) 2011-2019 by the MyPaint Development Team.
4#
5# This program is free software; you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation; either version 2 of the License, or
8# (at your option) any later version.
9
10
11"""Whole-tree-level layer classes and functions"""
12
13
14## Imports
15
16from __future__ import division, print_function
17
18import re
19import logging
20from copy import copy
21from copy import deepcopy
22import os.path
23from warnings import warn
24import contextlib
25
26from lib.gibindings import GdkPixbuf
27from lib.gibindings import GLib
28import numpy as np
29
30from lib.eotf import eotf
31from lib.gettext import C_
32import lib.mypaintlib
33import lib.tiledsurface as tiledsurface
34from lib.tiledsurface import TileAccessible
35from lib.tiledsurface import TileBlittable
36import lib.helpers as helpers
37from lib.observable import event
38import lib.pixbuf
39import lib.cache
40from lib.modes import PASS_THROUGH_MODE
41from lib.modes import MODES_DECREASING_BACKDROP_ALPHA
42from . import data
43from . import group
44from . import core
45from . import rendering
46import lib.feedback
47import lib.naming
48from lib.pycompat import xrange
49
50
51logger = logging.getLogger(__name__)
52
53
54## Class defs
55
56
57class PlaceholderLayer (group.LayerStack):
58    """Trivial temporary placeholder layer, used for moves etc.
59
60    The layer stack architecture does not allow for the same layer
61    appearing twice in a tree structure. Layer operations therefore
62    require unique placeholder layers occasionally, typically when
63    swapping nodes in the tree or handling drags.
64    """
65
66    DEFAULT_NAME = C_(
67        "layer default names",
68        # TRANSLATORS: Short default name for temporary placeholder layers.
69        # TRANSLATORS: (The user should never see this except in error cases)
70        u"Placeholder",
71    )
72
73
74class RootLayerStack (group.LayerStack):
75    """Specialized document root layer stack
76
77    In addition to the basic lib.layer.group.LayerStack implementation,
78    this class's methods and properties provide:
79
80     * the document's background, using an internal BackgroundLayer;
81     * tile rendering for the doc via the regular rendering interface;
82     * special viewing modes (solo, previewing);
83     * the currently selected layer;
84     * path-based access to layers in the tree;
85     * a global symmetry axis for painting;
86     * manipulation of layer paths; and
87     * convenient iteration over the tree structure.
88
89    In other words, root layer stacks handle anything that needs
90    document-scale oversight of the tree structure to operate.  An
91    instance of this instantiated for the running app as part of the
92    primary `lib.document.Document` object.  The descendent layers of
93    this object are those that are presented as user-addressable layers
94    in the Layers panel.
95
96    Be careful to maintain global uniqueness of layers within the root
97    layer stack. If this isn't respected, then replacing an instance of
98    item which exists in two or more places in the tree will break that
99    layer's root reference and cause it to silently stop emitting
100    updates. Use a `PlaceholderLayer` to work around this, or just
101    reinstate the root ref when you're done juggling layers.
102
103    """
104
105    ## Class constants
106
107    DEFAULT_NAME = C_(
108        "layer default names",
109        u"Root",
110    )
111    INITIAL_MODE = lib.mypaintlib.CombineNormal
112    PERMITTED_MODES = {INITIAL_MODE}
113
114    ## Initialization
115
116    def __init__(self, doc=None,
117                 cache_size=lib.cache.DEFAULT_CACHE_SIZE,
118                 **kwargs):
119        """Construct, as part of a model
120
121        :param doc: The model document. May be None for testing.
122        :type doc: lib.document.Document
123        :param cache_size: size of the layer render cache
124        :type cache_size: int
125        """
126        super(RootLayerStack, self).__init__(**kwargs)
127        self.doc = doc
128        self._render_cache = lib.cache.LRUCache(capacity=cache_size)
129        # Background
130        default_bg = (255, 255, 255)
131        self._default_background = default_bg
132        self._background_layer = data.BackgroundLayer(default_bg)
133        self._background_visible = True
134        # Symmetry
135        self._symmetry_x = None
136        self._symmetry_y = None
137        self._symmetry_type = None
138        self._rot_symmetry_lines = 2
139        self._symmetry_active = False
140        # Special rendering state
141        self._current_layer_solo = False
142        self._current_layer_previewing = False
143        # Current layer
144        self._current_path = ()
145        # Temporary overlay for the current layer
146        self._current_layer_overlay = None
147        # Self-observation
148        self.layer_content_changed += self._render_cache_clear_area
149        self.layer_properties_changed += self._render_cache_clear
150        self.layer_deleted += self._render_cache_clear
151        self.layer_inserted += self._render_cache_clear
152        # Layer thumbnail updates
153        self.layer_content_changed += self._mark_layer_for_rethumb
154        self._rethumb_layers = []
155        self._rethumb_layers_timer_id = None
156
157    # Render cache management:
158
159    def _render_cache_get(self, key1, key2):
160        try:
161            cache2 = self._render_cache[key1]
162            return cache2[key2]
163        except KeyError:
164            pass
165        return None
166
167    def _render_cache_set(self, key1, key2, data):
168        try:
169            cache2 = self._render_cache[key1]
170        except KeyError:
171            cache2 = dict()  # it'll have ~MAX_MIPMAP_LEVEL items
172            self._render_cache[key1] = cache2
173        cache2[key2] = data
174
175    def _render_cache_clear_area(self, root, layer, x, y, w, h):
176        """Clears rendered tiles from the cache in a specific area."""
177
178        if (w <= 0) or (h <= 0):  # update all notifications
179            self._render_cache_clear()
180            return
181
182        n = lib.mypaintlib.TILE_SIZE
183        tx_min = x // n
184        tx_max = ((x + w) // n)
185        ty_min = y // n
186        ty_max = ((y + h) // n)
187        mipmap_level_max = lib.mypaintlib.MAX_MIPMAP_LEVEL
188
189        for tx in range(tx_min, tx_max + 1):
190            for ty in range(ty_min, ty_max + 1):
191                for level in range(0, mipmap_level_max + 1):
192                    fac = 2 ** level
193                    key = ((tx // fac), (ty // fac), level)
194                    self._render_cache.pop(key, None)
195
196    def _render_cache_clear(self, *_ignored):
197        """Clears all rendered tiles from the cache."""
198        self._render_cache.clear()
199
200    # Global ops:
201
202    def clear(self):
203        """Clear the layer and set the default background"""
204        super(RootLayerStack, self).clear()
205        self.set_background(self._default_background)
206        self.current_path = ()
207        self._render_cache_clear()
208
209    def ensure_populated(self, layer_class=None):
210        """Ensures that the stack is non-empty by making a new layer if needed
211
212        :param layer_class: The class of layer to add, if necessary
213        :type layer_class: LayerBase
214        :returns: The new layer instance, or None if nothing was created
215
216        >>> root = RootLayerStack(None); root
217        <RootLayerStack len=0>
218        >>> root.ensure_populated(layer_class=group.LayerStack); root
219        <LayerStack len=0>
220        <RootLayerStack len=1>
221
222        The default `layer_class` is the regular painting layer.
223
224        >>> root.clear(); root
225        <RootLayerStack len=0>
226        >>> root.ensure_populated(); root
227        <PaintingLayer>
228        <RootLayerStack len=1>
229
230        """
231        if layer_class is None:
232            layer_class = data.PaintingLayer
233        layer = None
234        if len(self) == 0:
235            layer = layer_class()
236            self.append(layer)
237            self._current_path = (0,)
238        return layer
239
240    def remove_empty_tiles(self):
241        """Removes empty tiles in all layers backed by a tiled surface.
242
243        :returns: Stats about the removal: (nremoved, ntotal)
244        :rtype: tuple
245
246        """
247        removed, total = (0, 0)
248        for path, layer in self.walk():
249            try:
250                remove_method = layer.remove_empty_tiles
251            except AttributeError:
252                continue
253            r, t = remove_method()
254            removed += r
255            total += t
256        logger.debug(
257            "remove_empty_tiles: removed %d of %d tiles",
258            removed, total,
259        )
260        return (removed, total)
261
262    ## Terminal root access
263
264    @property
265    def root(self):
266        """Layer stack root: itself, in this case"""
267        return self
268
269    @root.setter
270    def root(self, newroot):
271        raise ValueError("Cannot set the root of the root layer stack")
272
273    ## Info methods
274
275    def get_names(self):
276        """Returns the set of unique names of all descendents"""
277        return set((l.name for l in self.deepiter()))
278
279    ## Rendering: root stack API
280
281    def _get_render_background(self, spec):
282        """True if render() should render the internal background
283
284        :rtype: bool
285
286        This reflects the background visibility flag normally,
287        but the layer-previewing flag inverts its effect.
288        This has the effect of making the current layer
289        blink very appreciably when changing layers.
290
291        Solo mode never shows the background, currently.
292        If this changes, layer normalization and thumbnails will break.
293
294        See also: background_visible, current_layer_previewing.
295
296        """
297        if spec.background is not None:
298            return spec.background
299        if spec.previewing:
300            return not self._background_visible
301        elif spec.solo:
302            return False
303        else:
304            return self._background_visible
305
306    def get_render_is_opaque(self, spec=None):
307        """True if the rendering is known to be 100% opaque
308
309        :rtype: bool
310
311        The UI should draw its own checquered background if this is
312        false, and expect `render()` to write RGBA data with lots
313        of transparent areas.
314
315        Even if the special background layer is enabled, it may be
316        knocked out by certain compositing modes of layers above it.
317
318        """
319        if spec is None:
320            spec = rendering.Spec(
321                current=self.current,
322                previewing=self._current_layer_previewing,
323                solo=self._current_layer_solo,
324            )
325        if not self._get_render_background(spec):
326            return False
327        for path, layer in self.walk(bghit=True, visible=True):
328            if layer.mode in MODES_DECREASING_BACKDROP_ALPHA:
329                return False
330        return True
331
332    def layers_along_path(self, path):
333        """Yields all layers along a path, not including the root"""
334        if not path:
335            return
336        unused_path = list(path)
337        layer = self
338        while len(unused_path) > 0:
339            if not isinstance(layer, group.LayerStack):
340                break
341            idx = unused_path.pop(0)
342            if not (0 <= idx < len(layer)):
343                break
344            layer = layer[idx]
345            yield layer
346
347    def layers_along_or_under_path(self, path, no_hidden_descendants=False):
348        """All parents, and all descendents of a path."""
349        path = tuple(path)
350        hidden_paths = set()
351        for p, layer in self.walk():
352            if not (path[0:len(p)] == p or    # ancestor of p, or p itself
353                    p[0:len(path)] == path):  # descendent of p
354                continue
355            # Conditionally exclude hidden child layers
356            if no_hidden_descendants and len(p) > len(path):
357                if not layer.visible or p[:-1] in hidden_paths:
358                    if isinstance(layer, group.LayerStack):
359                        hidden_paths.add(p)
360                    continue
361            yield layer
362
363
364    def _get_render_spec(self, respect_solo=True, respect_previewing=True):
365        """Get a specification object for rendering the current state.
366
367        The returned spec object captures the current layer and all the
368        fiddly bits about layer preview and solo state, and exactly what
369        layers to render when one of those modes is active.
370
371        """
372        spec = rendering.Spec(current=self.current)
373
374        if respect_solo:
375            spec.solo = self._current_layer_solo
376        if respect_previewing:
377            spec.previewing = self._current_layer_previewing
378        if spec.solo or spec.previewing:
379            path = self.get_current_path()
380            spec.layers = set(self.layers_along_or_under_path(path))
381
382        return spec
383
384    def _get_backdrop_render_spec_for_layer(self, path):
385        """Get a render spec for the backdrop of a layer.
386
387        This method returns a spec object expressing the natural
388        rendering of the backdrop to a specific layer path. This is used
389        for extracts and subtractions.
390
391        The backdrop consists of all layers underneath the layer in
392        question, plus all of their parents.
393
394        """
395        seen_srclayer = False
396        backdrop_layers = set()
397        for p, layer in self.walk():
398            if path_startswith(p, path):
399                seen_srclayer = True
400            elif seen_srclayer or isinstance(layer, group.LayerStack):
401                backdrop_layers.add(layer)
402        # For the backdrop, use a default rendering, respecting
403        # all but transient effects.
404        bd_spec = self._get_render_spec(respect_previewing=False)
405        if bd_spec.layers is not None:
406            backdrop_layers.intersection_update(bd_spec.layers)
407        bd_spec.layers = backdrop_layers
408        return bd_spec
409
410    def render(self, surface, tiles, mipmap_level, overlay=None,
411               opaque_base_tile=None, filter=None, spec=None,
412               progress=None, background=None, alpha=None, **kwargs):
413        """Render a batch of tiles into a tile-addressable surface.
414
415        :param TileAccesible surface: The target surface.
416        :param iterable tiles: The tile indices to render into "surface".
417        :param int mipmap_level: downscale degree. Ensure tile indices match.
418        :param lib.layer.core.LayerBase overlay: A global overlay layer.
419        :param callable filter: Display filter (8bpc tile array mangler).
420        :param lib.layer.rendering.Spec spec: Explicit rendering spec.
421        :param lib.feedback.Progress progress: Feedback object.
422        :param bool background: Render the background? (None means natural).
423        :param bool alpha: Deprecated alias for "background" (reverse sense).
424        :param **kwargs: Extensibility.
425
426        This API may evolve to use only the "spec" argument rather than
427        the explicit overlay etc.
428
429        """
430        if progress is None:
431            progress = lib.feedback.Progress()
432        tiles = list(tiles)
433        progress.items = len(tiles)
434        if len(tiles) == 0:
435            progress.close()
436            return
437
438        if background is None and alpha is not None:
439            warn("Use 'background' instead of 'alpha'", DeprecationWarning)
440            background = not alpha
441
442        if spec is None:
443            spec = self._get_render_spec()
444        if overlay is not None:
445            spec.global_overlay = overlay
446        if background is not None:
447            spec.background = bool(background)
448
449        dst_has_alpha = not self.get_render_is_opaque(spec=spec)
450        ops = self.get_render_ops(spec)
451
452        target_surface_is_8bpc = False
453        use_cache = False
454        tx, ty = tiles[0]
455        with surface.tile_request(tx, ty, readonly=True) as sample_tile:
456            target_surface_is_8bpc = (sample_tile.dtype == 'uint8')
457            if target_surface_is_8bpc:
458                use_cache = spec.cacheable()
459        key2 = (id(opaque_base_tile), dst_has_alpha)
460
461        # Rendering loop.
462        # Keep this as tight as possible, and consider C++ parallelization.
463        tiledims = (tiledsurface.N, tiledsurface.N, 4)
464        dst_has_alpha_orig = dst_has_alpha
465        for tx, ty in tiles:
466            dst_8bpc_orig = None
467            dst_has_alpha = dst_has_alpha_orig
468            key1 = (tx, ty, mipmap_level)
469            cache_hit = False
470
471            with surface.tile_request(tx, ty, readonly=False) as dst:
472
473                # Twirl out any 8bpc target here,
474                # if the render cache is empty for this tile.
475                if target_surface_is_8bpc:
476                    dst_8bpc_orig = dst
477                    dst = None
478                    if use_cache:
479                        dst = self._render_cache_get(key1, key2)
480
481                    if dst is None:
482                        dst = np.zeros(tiledims, dtype='uint16')
483                    else:
484                        cache_hit = True  # note: dtype is now uint8
485
486                if not cache_hit:
487                    # Render to dst.
488                    # dst is a fix15 rgba tile
489
490                    dst_over_opaque_base = None
491                    if dst_has_alpha and opaque_base_tile is not None:
492                        dst_over_opaque_base = dst
493                        lib.mypaintlib.tile_copy_rgba16_into_rgba16(
494                            opaque_base_tile,
495                            dst_over_opaque_base,
496                        )
497                        dst = np.zeros(tiledims, dtype='uint16')
498
499                    # Process the ops list.
500                    self._process_ops_list(
501                        ops,
502                        dst, dst_has_alpha,
503                        tx, ty, mipmap_level,
504                    )
505
506                    if dst_over_opaque_base is not None:
507                        dst_has_alpha = False
508                        lib.mypaintlib.tile_combine(
509                            lib.mypaintlib.CombineNormal,
510                            dst, dst_over_opaque_base,
511                            False, 1.0,
512                        )
513                        dst = dst_over_opaque_base
514
515                # If the target tile is fix15 already, we're done.
516                if dst_8bpc_orig is None:
517                    continue
518
519                # Untwirl into the target 8bpc tile.
520                if not cache_hit:
521                    # Rendering just happened.
522                    # Convert to 8bpc, and maybe store.
523                    if dst_has_alpha:
524                        conv = lib.mypaintlib.tile_convert_rgba16_to_rgba8
525                    else:
526                        conv = lib.mypaintlib.tile_convert_rgbu16_to_rgbu8
527                    conv(dst, dst_8bpc_orig, eotf())
528
529                    if use_cache:
530                        self._render_cache_set(key1, key2, dst_8bpc_orig)
531                else:
532                    # An already 8pbc dst was loaded from the cache.
533                    # It will match dst_has_alpha already.
534                    dst_8bpc_orig[:] = dst
535
536                dst = dst_8bpc_orig
537
538                # Display filtering only happens when rendering
539                # 8bpc for the screen.
540                if filter is not None:
541                    filter(dst)
542
543            # end tile_request
544            progress += 1
545        progress.close()
546
547    def render_layer_preview(self, layer, size=256, bbox=None, **options):
548        """Render a standardized thumbnail/preview of a specific layer.
549
550        :param lib.layer.core.LayerBase layer: The layer to preview.
551        :param int size: Size of the output pixbuf.
552        :param tuple bbox: Rectangle to render (x, y, w, h).
553        :param **options: Passed to render().
554        :rtype: GdkPixbuf.Pixbuf
555
556        """
557        x, y, w, h = self._validate_layer_bbox_arg(layer, bbox)
558
559        mipmap_level = 0
560        while mipmap_level < lib.tiledsurface.MAX_MIPMAP_LEVEL:
561            if max(w, h) <= size:
562                break
563            mipmap_level += 1
564            x //= 2
565            y //= 2
566            w //= 2
567            h //= 2
568        w = max(1, w)
569        h = max(1, h)
570
571        spec = self._get_render_spec_for_layer(layer)
572
573        surface = lib.pixbufsurface.Surface(x, y, w, h)
574        surface.pixbuf.fill(0x00000000)
575        tiles = list(surface.get_tiles())
576        self.render(surface, tiles, mipmap_level, spec=spec, **options)
577
578        pixbuf = surface.pixbuf
579        assert pixbuf.get_width() == w
580        assert pixbuf.get_height() == h
581        if not ((w == size) or (h == size)):
582            pixbuf = helpers.scale_proportionally(pixbuf, size, size)
583        return pixbuf
584
585    def render_layer_as_pixbuf(self, layer, bbox=None, **options):
586        """Render a layer as a GdkPixbuf.
587
588        :param lib.layer.core.LayerBase layer: The layer to preview.
589        :param tuple bbox: Rectangle to render (x, y, w, h).
590        :param **options: Passed to render().
591        :rtype: GdkPixbuf.Pixbuf
592
593        The "layer" param must be a descendent layer or the root layer
594        stack itself.
595
596        The "bbox" parameter defaults to the natural data bounding box
597        of "layer", and has a minimum size of one tile.
598
599        """
600        x, y, w, h = self._validate_layer_bbox_arg(layer, bbox)
601        spec = self._get_render_spec_for_layer(layer)
602
603        surface = lib.pixbufsurface.Surface(x, y, w, h)
604        surface.pixbuf.fill(0x00000000)
605        tiles = list(surface.get_tiles())
606        self.render(surface, tiles, 0, spec=spec, **options)
607
608        pixbuf = surface.pixbuf
609        assert pixbuf.get_width() == w
610        assert pixbuf.get_height() == h
611        return pixbuf
612
613    def render_layer_to_png_file(self, layer, filename, bbox=None, **options):
614        """Render out to a PNG file. Used by LayerGroup.save_as_png()."""
615        bbox = self._validate_layer_bbox_arg(layer, bbox)
616        spec = self._get_render_spec_for_layer(layer)
617        spec.background = options.get("render_background")
618        rendering = _TileRenderWrapper(self, spec, use_cache=False)
619        if "alpha" not in options:
620            options["alpha"] = True
621        lib.surface.save_as_png(rendering, filename, *bbox, **options)
622
623    def get_tile_accessible_layer_rendering(self, layer):
624        """Get a TileAccessible temporary rendering of a sublayer.
625
626        :returns: A temporary rendering object with inbuilt tile cache.
627
628        The result is used to implement flood_fill for layer types
629        which don't contain their own tile-accessible data.
630
631        """
632        spec = self._get_render_spec_for_layer(
633            layer, no_hidden_descendants=True
634        )
635        rendering = _TileRenderWrapper(self, spec)
636        return rendering
637
638    def _get_render_spec_for_layer(self, layer, no_hidden_descendants=False):
639        """Get a standardized rendering spec for a single layer.
640
641        :param layer: The layer to render, can be the RootLayerStack.
642        :rtype: lib.layer.rendering.Spec
643
644        This method prepares a standardized rendering spec that shows a
645        specific sublayer by itself, or the root stack complete with
646        background. The spec returned does not introduce any special
647        effects, and ignores any special viewing modes. It is suitable
648        for the standardized "render_layer_*()" methods.
649
650        """
651        spec = self._get_render_spec(
652            respect_solo=False,
653            respect_previewing=False,
654        )
655        if layer is not self:
656            layer_path = self.deepindex(layer)
657            if layer_path is None:
658                raise ValueError(
659                    "Layer is not a descendent of this RootLayerStack.",
660                )
661            layers = self.layers_along_or_under_path(
662                layer_path, no_hidden_descendants)
663            spec.layers = set(layers)
664            spec.current = layer
665            spec.solo = True
666        return spec
667
668    def render_single_tile(self, dst, dst_has_alpha,
669                           tx, ty, mipmap_level=0,
670                           layer=None, spec=None, ops=None):
671        """Render one tile in a standardized way (by default).
672
673        It's used in fix15 mode for enabling flood fill when the source
674        is a group, or when sample_merged is turned on.
675
676        """
677        if ops is None:
678            if spec is None:
679                if layer is None:
680                    layer = self.current
681                spec = self._get_render_spec_for_layer(layer)
682            ops = self.get_render_ops(spec)
683
684        dst_is_8bpc = (dst.dtype == 'uint8')
685        if dst_is_8bpc:
686            dst_8bpc_orig = dst
687            tiledims = (tiledsurface.N, tiledsurface.N, 4)
688            dst = np.zeros(tiledims, dtype='uint16')
689
690        self._process_ops_list(ops, dst, dst_has_alpha, tx, ty, mipmap_level)
691
692        if dst_is_8bpc:
693            if dst_has_alpha:
694                conv = lib.mypaintlib.tile_convert_rgba16_to_rgba8
695            else:
696                conv = lib.mypaintlib.tile_convert_rgbu16_to_rgbu8
697            conv(dst, dst_8bpc_orig, eotf())
698            dst = dst_8bpc_orig
699
700    def _validate_layer_bbox_arg(self, layer, bbox,
701                                 min_size=lib.tiledsurface.TILE_SIZE):
702        """Check a bbox arg, defaulting it to the data size of a layer."""
703        min_size = int(min_size)
704        if bbox is not None:
705            x, y, w, h = (int(n) for n in bbox)
706        else:
707            x, y, w, h = layer.get_bbox()
708        if w == 0 or h == 0:
709            x = 0
710            y = 0
711            w = 1
712            h = 1
713        w = max(min_size, w)
714        h = max(min_size, h)
715        return (x, y, w, h)
716
717    @staticmethod
718    def _process_ops_list(ops, dst, dst_has_alpha, tx, ty, mipmap_level):
719        """Process a list of ops to render a tile. fix15 data only!"""
720        # FIXME: should this be expanded to cover caching and 8bpc
721        # targets? It would save on some code duplication elsewhere.
722        # On the other hand, this is sort of what a parallelized,
723        # GIL-holding C++ loop body might look like.
724
725        stack = []
726        for (opcode, opdata, mode, opacity) in ops:
727            if opcode == rendering.Opcode.COMPOSITE:
728                opdata.composite_tile(
729                    dst, dst_has_alpha, tx, ty,
730                    mipmap_level=mipmap_level,
731                    mode=mode, opacity=opacity,
732                )
733            elif opcode == rendering.Opcode.BLIT:
734                opdata.blit_tile_into(
735                    dst, dst_has_alpha, tx, ty,
736                    mipmap_level,
737                )
738            elif opcode == rendering.Opcode.PUSH:
739                stack.append((dst, dst_has_alpha))
740                tiledims = (tiledsurface.N, tiledsurface.N, 4)
741                dst = np.zeros(tiledims, dtype='uint16')
742                dst_has_alpha = True
743            elif opcode == rendering.Opcode.POP:
744                src = dst
745                (dst, dst_has_alpha) = stack.pop(-1)
746                lib.mypaintlib.tile_combine(
747                    mode,
748                    src, dst, dst_has_alpha,
749                    opacity,
750                )
751            else:
752                raise RuntimeError(
753                    "Unknown lib.layer.rendering.Opcode: %r",
754                    opcode,
755                )
756        if len(stack) > 0:
757            raise ValueError(
758                "Ops list contains more PUSH operations "
759                "than POPs. Rendering is incomplete."
760            )
761
762    ## Renderable implementation
763
764    def get_render_ops(self, spec):
765        """Get rendering instructions."""
766        ops = []
767        if self._get_render_background(spec):
768            bg_opcode = rendering.Opcode.BLIT
769            bg_surf = self._background_layer._surface
770            ops.append((bg_opcode, bg_surf, None, None))
771        for child_layer in reversed(self):
772            ops.extend(child_layer.get_render_ops(spec))
773        if spec.global_overlay is not None:
774            ops.extend(spec.global_overlay.get_render_ops(spec))
775        return ops
776
777    ## Symmetry axis
778
779    @property
780    def symmetry_active(self):
781        """Whether symmetrical painting is active.
782
783        This is a convenience property for part of
784        the state managed by `set_symmetry_state()`.
785        """
786        return self._symmetry_active
787
788    @symmetry_active.setter
789    def symmetry_active(self, active):
790        if self._symmetry_x is None:
791            raise ValueError(
792                "UI code must set a non-Null symmetry_x "
793                "before activating symmetrical painting."
794            )
795        if self._symmetry_y is None:
796            raise ValueError(
797                "UI code must set a non-Null symmetry_y "
798                "before activating symmetrical painting."
799            )
800        if self._symmetry_type is None:
801            raise ValueError(
802                "UI code must set a non-Null symmetry_type "
803                "before activating symmetrical painting."
804            )
805        self.set_symmetry_state(
806            active,
807            self._symmetry_x, self._symmetry_y,
808            self._symmetry_type, self.rot_symmetry_lines,
809        )
810
811    # should be combined into one prop for less event firing
812    @property
813    def symmetry_y(self):
814        """The active painting symmetry Y axis value
815
816        The `symmetry_y` property may be set to None.
817        This indicates the initial state of a document when
818        it has been newly created, or newly opened from a file.
819
820        Setting the property to a value forces `symmetry_active` on,
821        and setting it to ``None`` forces `symmetry_active` off.
822        In both bases, only one `symmetry_state_changed` gets emitted.
823
824        This is a convenience property for part of
825        the state managed by `set_symmetry_state()`.
826        """
827        return self._symmetry_y
828
829    @property
830    def symmetry_x(self):
831        """The active painting symmetry X axis value
832
833        The `symmetry_x` property may be set to None.
834        This indicates the initial state of a document when
835        it has been newly created, or newly opened from a file.
836
837        Setting the property to a value forces `symmetry_active` on,
838        and setting it to ``None`` forces `symmetry_active` off.
839        In both bases, only one `symmetry_state_changed` gets emitted.
840
841        This is a convenience property for part of
842        the state managed by `set_symmetry_state()`.
843        """
844        return self._symmetry_x
845
846    @symmetry_x.setter
847    def symmetry_x(self, x):
848        if x is None:
849            self.set_symmetry_state(False, None, None, None, None)
850        else:
851            self.set_symmetry_state(
852                True,
853                x,
854                self._symmetry_y,
855                self._symmetry_type,
856                self._rot_symmetry_lines
857            )
858
859    @symmetry_y.setter
860    def symmetry_y(self, y):
861        if y is None:
862            self.set_symmetry_state(False, None, None, None, None)
863        else:
864            self.set_symmetry_state(
865                True,
866                self._symmetry_x,
867                y,
868                self._symmetry_type,
869                self._rot_symmetry_lines
870            )
871
872    @property
873    def symmetry_type(self):
874        return self._symmetry_type
875
876    @symmetry_type.setter
877    def symmetry_type(self, symmetry_type):
878        if symmetry_type is None:
879            self.set_symmetry_state(False, None, None, None, None)
880        else:
881            self.set_symmetry_state(
882                True,
883                self._symmetry_x,
884                self._symmetry_y,
885                symmetry_type,
886                self._rot_symmetry_lines
887            )
888
889    @property
890    def rot_symmetry_lines(self):
891        return self._symmetry_type
892
893    @rot_symmetry_lines.setter
894    def rot_symmetry_lines(self, rot_symmetry_lines):
895        if rot_symmetry_lines is None:
896            self.set_symmetry_state(False, None, None, None, None)
897        else:
898            self.set_symmetry_state(
899                True,
900                self._symmetry_x,
901                self._symmetry_y,
902                self._symmetry_type,
903                rot_symmetry_lines
904            )
905
906    def set_symmetry_state(self, active, center_x, center_y,
907                           symmetry_type, rot_symmetry_lines):
908        """Set the central, propagated, symmetry axis and active flag.
909
910        The root layer stack specialization manages a central state,
911        which is propagated to the current layer automatically.
912
913        See `LayerBase.set_symmetry_state` for the params.
914        This override allows the shared `center_x` to be ``None``:
915        see `symmetry_x` for what that means.
916
917        """
918        active = bool(active)
919        if center_x is not None:
920            center_x = round(float(center_x))
921        if center_y is not None:
922            center_y = round(float(center_y))
923        if symmetry_type is not None:
924            symmetry_type = int(symmetry_type)
925        if rot_symmetry_lines is not None:
926            rot_symmetry_lines = int(rot_symmetry_lines)
927
928        oldstate = (
929            self._symmetry_active,
930            self._symmetry_x,
931            self._symmetry_y,
932            self._symmetry_type,
933            self._rot_symmetry_lines,
934        )
935        newstate = (
936            active,
937            center_x,
938            center_y,
939            symmetry_type,
940            rot_symmetry_lines,
941        )
942        if oldstate == newstate:
943            return
944        self._symmetry_active = active
945        self._symmetry_x = center_x
946        self._symmetry_y = center_y
947        self._symmetry_type = symmetry_type
948        self._rot_symmetry_lines = rot_symmetry_lines
949        current = self.get_current()
950        if current is not self:
951            self._propagate_symmetry_state(current)
952        self.symmetry_state_changed(
953            active,
954            center_x,
955            center_y,
956            symmetry_type,
957            rot_symmetry_lines
958        )
959
960    def _propagate_symmetry_state(self, layer):
961        """Copy the symmetry state to the a descendant layer"""
962        assert layer is not self
963        if None in {self._symmetry_x, self._symmetry_y, self._symmetry_type}:
964            return
965        layer.set_symmetry_state(
966            self._symmetry_active,
967            self._symmetry_x,
968            self._symmetry_y,
969            self._symmetry_type,
970            self._rot_symmetry_lines
971        )
972
973    @event
974    def symmetry_state_changed(self, active, x, y,
975                               symmetry_type, rot_symmetry_lines):
976        """Event: symmetry axis was changed, or was toggled
977
978        :param bool active: updated `symmetry_active` value
979        :param int x: new symmetry reference point X
980        :param int y: new symmetry reference point Y
981        :param int symmetry_type: symmetry type
982        :param int rot_symmetry_lines: new number of lines
983        """
984
985    ## Current layer
986
987    def get_current_path(self):
988        """Get the current layer's path
989
990        :rtype: tuple
991
992        If the current path was set to a path which was invalid at the
993        time of setting, the returned value is always an empty tuple for
994        convenience of casting. This is however an invalid path for
995        addressing sub-layers.
996        """
997        if not self._current_path:
998            return ()
999        return self._current_path
1000
1001    def set_current_path(self, path):
1002        """Set the current layer path
1003
1004        :param path: The path to use; will be trimmed until it fits
1005        :type path: tuple
1006        """
1007        if len(self) == 0:
1008            self._current_path = None
1009            self.current_path_updated(())
1010            return
1011        path = tuple(path)
1012        while len(path) > 0:
1013            layer = self.deepget(path)
1014            if layer is not None:
1015                self._propagate_symmetry_state(layer)
1016                break
1017            path = path[:-1]
1018        if len(path) == 0:
1019            path = None
1020        self._current_path = path
1021        self.current_path_updated(path)
1022
1023    current_path = property(get_current_path, set_current_path)
1024
1025    def get_current(self):
1026        """Get the current layer (also exposed as a read-only property)
1027
1028        This returns the root layer stack itself if the current path
1029        doesn't address a sub-layer.
1030        """
1031        return self.deepget(self.get_current_path(), self)
1032
1033    current = property(get_current)
1034
1035    ## The background layer
1036
1037    @property
1038    def background_layer(self):
1039        """The background layer (accessor)"""
1040        return self._background_layer
1041
1042    def set_background(self, obj, make_default=False):
1043        """Set the background layer's surface from an object
1044
1045        :param obj: Background object
1046        :type obj: layer.data.BackgroundLayer or tuple or numpy array
1047        :param make_default: make this the default bg for clear()
1048        :type make_default: bool
1049
1050        The background object argument `obj` can be a background layer,
1051        or an RGB triple (uint8), or a HxWx4 or HxWx3 numpy array which
1052        can be either uint8 or uint16.
1053
1054        Setting the background issues a full redraw for the root layer,
1055        and also issues the `background_changed` event. The background
1056        will also be made visible if it isn't already.
1057        """
1058        if isinstance(obj, data.BackgroundLayer):
1059            obj = obj._surface
1060        if not isinstance(obj, tiledsurface.Background):
1061            if isinstance(obj, GdkPixbuf.Pixbuf):
1062                obj = helpers.gdkpixbuf2numpy(obj)
1063            obj = tiledsurface.Background(obj)
1064        self._background_layer.set_surface(obj)
1065        if make_default:
1066            self._default_background = obj
1067        self.background_changed()
1068        if not self._background_visible:
1069            self._background_visible = True
1070            self.background_visible_changed()
1071        self.layer_content_changed(self, 0, 0, 0, 0)
1072
1073    @event
1074    def background_changed(self):
1075        """Event: background layer data has changed"""
1076
1077    @property
1078    def background_visible(self):
1079        """Whether the background is visible
1080
1081        Accepts only values which can be converted to bool.  Changing
1082        the background visibility flag issues a full redraw for the root
1083        layer, and also issues the `background_changed` event.
1084        """
1085        return bool(self._background_visible)
1086
1087    @background_visible.setter
1088    def background_visible(self, value):
1089        value = bool(value)
1090        old_value = self._background_visible
1091        self._background_visible = value
1092        if value != old_value:
1093            self.background_visible_changed()
1094            self.layer_content_changed(self, 0, 0, 0, 0)
1095
1096    @event
1097    def background_visible_changed(self):
1098        """Event: the background visibility flag has changed"""
1099
1100    ## Temporary overlays for the current layer (not saved)
1101
1102    @property
1103    def current_layer_overlay(self):
1104        """A temporary overlay layer for the current layer.
1105
1106        This isn't saved as part of the document, and strictly speaking
1107        it exists outside the doument tree. If it is present, then
1108        during rendering it is composited onto the current painting
1109        layer in isolation. The effect is as if the overlay were part of
1110        the current painting layer.
1111
1112        The intent of this layer type is to collect together and preview
1113        sets of updates to the current layer in response to user input.
1114        The updates can then be applied all together by an action.
1115        Another possibility might be for brush preview special effects.
1116
1117        The current layer overlay can be a group, which allows capture
1118        of masked drawing. If you want updates to propagate back to the
1119        root, the group needs to be set as the ``current_layer_overlay``
1120        first. Otherwise, ``root``s won't be hooked up and managed in
1121        the right order.
1122
1123        >>> root = RootLayerStack()
1124        >>> root.append(data.SimplePaintingLayer())
1125        >>> root.append(data.SimplePaintingLayer())
1126        >>> root.set_current_path([1])
1127        >>> ovgroup = group.LayerStack()
1128        >>> root.current_layer_overlay = ovgroup
1129        >>> ovdata1 = data.SimplePaintingLayer()
1130        >>> ovdata2 = data.SimplePaintingLayer()
1131        >>> ovgroup.append(ovdata1)
1132        >>> ovgroup.append(ovdata2)
1133        >>> change_count = 0
1134        >>> def changed(*a):
1135        ...     global change_count
1136        ...     change_count += 1
1137        >>> root.layer_content_changed += changed
1138        >>> ovdata1.clear()
1139        >>> ovdata2.clear()
1140        >>> change_count
1141        2
1142
1143        Setting the overlay or setting it to None generates content
1144        change notifications too.
1145
1146        >>> root.current_layer_overlay = None
1147        >>> root.current_layer_overlay = data.SimplePaintingLayer()
1148        >>> change_count
1149        4
1150
1151        """
1152        return self._current_layer_overlay
1153
1154    @current_layer_overlay.setter
1155    def current_layer_overlay(self, overlay):
1156        old_overlay = self._current_layer_overlay
1157        self._current_layer_overlay = overlay
1158        self.current_layer_overlay_changed(old_overlay)
1159
1160        updates = []
1161        if old_overlay is not None:
1162            old_overlay.root = None
1163            updates.append(old_overlay.get_full_redraw_bbox())
1164        if overlay is not None:
1165            overlay.root = self  # for redraw announcements
1166            updates.append(overlay.get_full_redraw_bbox())
1167
1168        if updates:
1169            update_bbox = tuple(core.combine_redraws(updates))
1170            self.layer_content_changed(self, *update_bbox)
1171
1172    @event
1173    def current_layer_overlay_changed(self, old):
1174        """Event: current_layer_overlay was altered"""
1175
1176    ## Layer Solo toggle (not saved)
1177
1178    @property
1179    def current_layer_solo(self):
1180        """Layer-solo state for the document
1181
1182        Accepts only values which can be converted to bool.
1183        Altering this property issues the `current_layer_solo_changed`
1184        event, and a full `layer_content_changed` for the root stack.
1185        """
1186        return self._current_layer_solo
1187
1188    @current_layer_solo.setter
1189    def current_layer_solo(self, value):
1190        # TODO: make this undoable
1191        value = bool(value)
1192        old_value = self._current_layer_solo
1193        self._current_layer_solo = value
1194        if value != old_value:
1195            self.current_layer_solo_changed()
1196            self.layer_content_changed(self, 0, 0, 0, 0)
1197
1198    @event
1199    def current_layer_solo_changed(self):
1200        """Event: current_layer_solo was altered"""
1201
1202    ## Current layer temporary preview state (not saved, used for blink)
1203
1204    @property
1205    def current_layer_previewing(self):
1206        """Layer-previewing state, as used when blinking a layer
1207
1208        Accepts only values which can be converted to bool.  Altering
1209        this property calls `current_layer_previewing_changed` and also
1210        issues a full `layer_content_changed` for the root stack.
1211        """
1212        return self._current_layer_previewing
1213
1214    @current_layer_previewing.setter
1215    def current_layer_previewing(self, value):
1216        """Layer-previewing state, as used when blinking a layer"""
1217        value = bool(value)
1218        old_value = self._current_layer_previewing
1219        self._current_layer_previewing = value
1220        if value != old_value:
1221            self.current_layer_previewing_changed()
1222            self.layer_content_changed(self, 0, 0, 0, 0)
1223
1224    @event
1225    def current_layer_previewing_changed(self):
1226        """Event: current_layer_previewing was altered"""
1227
1228    ## Layer naming within the tree
1229
1230    def get_unique_name(self, layer):
1231        """Get a unique name for a layer to use
1232
1233        :param LayerBase layer: Any layer
1234        :rtype: unicode
1235        :returns: A unique name
1236
1237        The returned name is guaranteed not to occur in the tree.  This
1238        method can be used before or after the layer is inserted into
1239        the stack.
1240        """
1241        existing = {l.name for path, l in self.walk()
1242                    if l is not layer
1243                    and l.name is not None}
1244        blank = re.compile(r'^\s*$')
1245        newname = layer._name
1246        if newname is None or blank.match(newname):
1247            newname = layer.DEFAULT_NAME
1248        return lib.naming.make_unique_name(newname, existing)
1249
1250    ## Layer path manipulation
1251
1252    def path_above(self, path, insert=False):
1253        """Return the path for the layer stacked above a given path
1254
1255        :param path: a layer path
1256        :type path: list or tuple
1257        :param insert: get an insertion path
1258        :type insert: bool
1259        :return: the layer above `path` in walk order
1260        :rtype: tuple
1261
1262        Normally this is used for locating the layer above a given node
1263        in the layers stack as the user sees it in a typical user
1264        interface:
1265
1266          >>> root = RootLayerStack(doc=None)
1267          >>> for p, l in [ ([0], data.PaintingLayer()),
1268          ...               ([1], group.LayerStack()),
1269          ...               ([1, 0], group.LayerStack()),
1270          ...               ([1, 0, 0], group.LayerStack()),
1271          ...               ([1, 0, 0, 0], data.PaintingLayer()),
1272          ...               ([1, 1], data.PaintingLayer()) ]:
1273          ...    root.deepinsert(p, l)
1274          >>> root.path_above([1])
1275          (0,)
1276
1277        Ascending the stack using this method can enter and leave
1278        subtrees:
1279
1280          >>> root.path_above([1, 1])
1281          (1, 0, 0, 0)
1282          >>> root.path_above([1, 0, 0, 0])
1283          (1, 0, 0)
1284
1285        There is no existing path above the topmost node in the stack:
1286
1287          >>> root.path_above([0]) is None
1288          True
1289
1290        This method can also be used to get a path for use with
1291        `deepinsert()` which will allow insertion above a particular
1292        existing layer.  Normally this is the same path as the input,
1293
1294          >>> root.path_above([0, 1], insert=True)
1295          (0, 1)
1296
1297        however for nonexistent paths, you're guaranteed to get back a
1298        valid insertion path:
1299
1300          >>> root.path_above([42, 1, 101], insert=True)
1301          (0,)
1302
1303        which of necessity is the insertion point for a new layer at the
1304        very top of the stack.
1305        """
1306        path = tuple(path)
1307        if len(path) == 0:
1308            raise ValueError("Path identifies the root stack")
1309        if insert:
1310            # Same sanity checks as for path_below()
1311            parent_path, index = path[:-1], path[-1]
1312            parent = self.deepget(parent_path, None)
1313            if parent is None:
1314                return (0,)
1315            else:
1316                index = max(0, index)
1317                return tuple(list(parent_path) + [index])
1318        p_prev = None
1319        for p, l in self.walk():
1320            p = tuple(p)
1321            if path == p:
1322                return p_prev
1323            p_prev = p
1324        return None
1325
1326    def path_below(self, path, insert=False):
1327        """Return the path for the layer stacked below a given path
1328
1329        :param path: a layer path
1330        :type path: list or tuple
1331        :param insert: get an insertion path
1332        :type insert: bool
1333        :return: the layer below `path` in walk order
1334        :rtype: tuple or None
1335
1336        This method is the inverse of `path_above()`: it normally
1337        returns the tree path below its `path` as the user would see it
1338        in a typical user interface:
1339
1340          >>> root = RootLayerStack(doc=None)
1341          >>> for p, l in [ ([0], data.PaintingLayer()),
1342          ...               ([1], group.LayerStack()),
1343          ...               ([1, 0], group.LayerStack()),
1344          ...               ([1, 0, 0], group.LayerStack()),
1345          ...               ([1, 0, 0, 0], data.PaintingLayer()),
1346          ...               ([1, 1], data.PaintingLayer()) ]:
1347          ...    root.deepinsert(p, l)
1348          >>> root.path_below([0])
1349          (1,)
1350
1351        Descending the stack using this method can enter and leave
1352        subtrees:
1353
1354          >>> root.path_below([1, 0])
1355          (1, 0, 0)
1356          >>> root.path_below([1, 0, 0, 0])
1357          (1, 1)
1358
1359        There is no path below the lowest addressable layer:
1360
1361          >>> root.path_below([1, 1]) is None
1362          True
1363
1364        Asking for an insertion path tries to get you somewhere to put a
1365        new layer that would make intuitive sense.  For most kinds of
1366        layer, that means one at the same level as the reference point
1367
1368          >>> root.path_below([0], insert=True)
1369          (1,)
1370          >>> root.path_below([1, 1], insert=True)
1371          (1, 2)
1372          >>> root.path_below([1, 0, 0, 0], insert=True)
1373          (1, 0, 0, 1)
1374
1375        However for sub-stacks, the insert-path "below" the stack is
1376        that for a new node as the stack's top child node
1377
1378          >>> root.path_below([1, 0], insert=True)
1379          (1, 0, 0)
1380
1381        Another exception to the general rule is that invalid paths
1382        always have an insertion path "below" them:
1383
1384          >> root.path_below([999, 42, 67], insert=True)
1385          (2,)
1386
1387        although this of necessity returns the insertion point for a new
1388        layer at the very bottom of the stack.
1389        """
1390        path = tuple(path)
1391        if len(path) == 0:
1392            raise ValueError("Path identifies the root stack")
1393        if insert:
1394            parent_path, index = path[:-1], path[-1]
1395            parent = self.deepget(parent_path, None)
1396            if parent is None:
1397                return (len(self),)
1398            elif isinstance(self.deepget(path, None), group.LayerStack):
1399                return path + (0,)
1400            else:
1401                index = min(len(parent), index + 1)
1402                return parent_path + (index,)
1403        p_prev = None
1404        for p, l in self.walk():
1405            p = tuple(p)
1406            if path == p_prev:
1407                return p
1408            p_prev = p
1409        return None
1410
1411    ## Layer bubbling
1412
1413    def _bubble_layer(self, path, upstack):
1414        """Move a layer up or down, preserving the tree structure
1415
1416        Parameters and return values are the same as for the public
1417        methods (`bubble_layer_up()`, `bubble_layer_down()`), with the
1418        following addition:
1419
1420        :param upstack: true to bubble up, false to bubble down
1421        """
1422        path = tuple(path)
1423        if len(path) == 0:
1424            raise ValueError("Cannot reposition the root of the stack")
1425
1426        parent_path, index = path[:-1], path[-1]
1427        parent = self.deepget(parent_path, self)
1428        assert index < len(parent)
1429        assert index > -1
1430
1431        # Collapse sub-stacks when bubbling them (not sure about this)
1432        if False:
1433            layer = self.deepget(path)
1434            assert layer is not None
1435            if isinstance(layer, group.LayerStack) and len(path) > 0:
1436                self.collapse_layer(path)
1437
1438        # The layer to be moved may already be at the end of its stack
1439        # in the direction we want; if so, remove it then insert it
1440        # one place beyond its parent in the bubble direction.
1441        end_index = 0 if upstack else (len(parent) - 1)
1442        if index == end_index:
1443            if parent is self:
1444                return False
1445            grandparent_path = parent_path[:-1]
1446            grandparent = self.deepget(grandparent_path, self)
1447            parent_index = grandparent.index(parent)
1448            layer = parent.pop(index)
1449            beyond_parent_index = parent_index
1450            if not upstack:
1451                beyond_parent_index += 1
1452            if len(grandparent_path) > 0:
1453                self.expand_layer(grandparent_path)
1454            grandparent.insert(beyond_parent_index, layer)
1455            return True
1456
1457        # Move the layer within its current parent
1458        new_index = index + (-1 if upstack else 1)
1459        if new_index < len(parent) and new_index > -1:
1460            # A sibling layer is already at the intended position
1461            sibling = parent[new_index]
1462            if isinstance(sibling, group.LayerStack):
1463                # Ascend: remove layer & put it at the near end
1464                # of the sibling stack
1465                sibling_path = parent_path + (new_index,)
1466                self.expand_layer(sibling_path)
1467                layer = parent.pop(index)
1468                if upstack:
1469                    sibling.append(layer)
1470                else:
1471                    sibling.insert(0, layer)
1472                return True
1473            else:
1474                # Swap positions with the sibling layer.
1475                # Use a placeholder, otherwise the root ref will be
1476                # lost.
1477                layer = parent[index]
1478                placeholder = PlaceholderLayer(name="swap")
1479                parent[index] = placeholder
1480                parent[new_index] = layer
1481                parent[index] = sibling
1482                return True
1483        else:
1484            # Nothing there, move to the end of this branch
1485            layer = parent.pop(index)
1486            if upstack:
1487                parent.insert(0, layer)
1488            else:
1489                parent.append(layer)
1490            return True
1491
1492    @event
1493    def collapse_layer(self, path):
1494        """Event: request that the UI collapse a given path"""
1495
1496    @event
1497    def expand_layer(self, path):
1498        """Event: request that the UI expand a given path"""
1499
1500    def bubble_layer_up(self, path):
1501        """Move a layer up through the stack
1502
1503        :param path: Layer path identifying the layer to move
1504        :returns: True if the stack structure was modified
1505
1506        Bubbling follows the layout of the tree and preserves its
1507        structure apart from the layers touched by the move, so it can
1508        be driven by the keyboard usefully. `bubble_layer_down()` is the
1509        exact inverse of this operation.
1510
1511        These methods assume the existence of a UI which lays out layers
1512        from top to bottom down the page, and which shows nodes or rows
1513        for LayerStacks (groups) before their contents.  If the path
1514        identifies a substack, the substack is moved as a whole.
1515
1516        Bubbling layers may issue several layer_inserted and
1517        layer_deleted events depending on what's moved, and may alter
1518        the current path too (see current_path_changed).
1519        """
1520        old_current = self.current
1521        modified = self._bubble_layer(path, True)
1522        if modified and old_current:
1523            self.current_path = self.canonpath(layer=old_current)
1524        return modified
1525
1526    def bubble_layer_down(self, path):
1527        """Move a layer down through the stack
1528
1529        :param path: Layer path identifying the layer to move
1530        :returns: True if the stack structure was modified
1531
1532        This is the inverse operation to bubbling a layer up.
1533        Parameters, notifications, and return values are the same as
1534        those for `bubble_layer_up()`.
1535        """
1536        old_current = self.current
1537        modified = self._bubble_layer(path, False)
1538        if modified and old_current:
1539            self.current_path = self.canonpath(layer=old_current)
1540        return modified
1541
1542    ## Simplified tree storage and access
1543
1544    # We use a path concept that's similar to GtkTreePath's, but almost like a
1545    # key/value store if this is the root layer stack.
1546
1547    def walk(self, visible=None, bghit=None):
1548        """Walks the tree, listing addressable layers & their paths
1549
1550        The parameters control how the walk operates as well as limiting
1551        its generated output.
1552
1553        :param visible: Only visible layers
1554        :type visible: bool
1555        :param bghit: Only layers compositing directly on the background
1556        :type bghit: bool
1557        :returns: Iterator yielding ``(path, layer)`` tuples
1558        :rtype: collections.Iterable
1559
1560        Layer substacks are listed before their contents, but the root
1561        of the walk is always excluded::
1562
1563            >>> from . import data
1564            >>> root = RootLayerStack(doc=None)
1565            >>> for p, l in [([0], data.PaintingLayer()),
1566            ...              ([1], group.LayerStack(name="A")),
1567            ...              ([1,0], data.PaintingLayer(name="B")),
1568            ...              ([1,1], data.PaintingLayer()),
1569            ...              ([2], data.PaintingLayer(name="C"))]:
1570            ...     root.deepinsert(p, l)
1571            >>> walk = list(root.walk())
1572            >>> root in {l for p, l in walk}
1573            False
1574            >>> walk[1]  # doctest: +ELLIPSIS
1575            ((1,), <LayerStack len=2 ...'A'>)
1576            >>> walk[2]  # doctest: +ELLIPSIS
1577            ((1, 0), <PaintingLayer ...'B'>)
1578
1579        The default behaviour is to return all layers.  If `visible`
1580        is true, hidden layers are excluded.  This excludes child layers
1581        of invisible layer stacks as well as the invisible stacks
1582        themselves.
1583
1584            >>> root.deepget([0]).visible = False
1585            >>> root.deepget([1]).visible = False
1586            >>> list(root.walk(visible=True))  # doctest: +ELLIPSIS
1587            [((2,), <PaintingLayer ...'C'>)]
1588
1589        If `bghit` is true, layers which could never affect the special
1590        background layer are excluded from the listing.  Specifically,
1591        all children of isolated layers are excluded, but not the
1592        isolated layers themselves.
1593
1594            >>> root.deepget([1]).mode = lib.mypaintlib.CombineMultiply
1595            >>> walk = list(root.walk(bghit=True))
1596            >>> root.deepget([1]) in {l for p, l in walk}
1597            True
1598            >>> root.deepget([1, 0]) in {l for p, l in walk}
1599            False
1600
1601        The special background layer itself is never returned by walk().
1602        """
1603        queue = [((i,), c) for i, c in enumerate(self)]
1604        while len(queue) > 0:
1605            path, layer = queue.pop(0)
1606            if visible and not layer.visible:
1607                continue
1608            yield (path, layer)
1609            if not isinstance(layer, group.LayerStack):
1610                continue
1611            if bghit and (layer.mode != PASS_THROUGH_MODE):
1612                continue
1613            queue[:0] = [(path + (i,), c) for i, c in enumerate(layer)]
1614
1615    def deepiter(self, visible=None):
1616        """Iterates across all descendents of the stack
1617
1618        >>> from . import test
1619        >>> stack, leaves = test.make_test_stack()
1620        >>> len(list(stack.deepiter()))
1621        8
1622        >>> len(set(stack.deepiter())) == len(list(stack.deepiter())) # no dups
1623        True
1624        >>> stack not in stack.deepiter()
1625        True
1626        >>> () not in stack.deepiter()
1627        True
1628        >>> leaves[0] in stack.deepiter()
1629        True
1630        """
1631        return (t[1] for t in self.walk(visible=visible))
1632
1633    def deepget(self, path, default=None):
1634        """Gets a layer based on its path
1635
1636        >>> from . import test
1637        >>> stack, leaves = test.make_test_stack()
1638        >>> stack.deepget(()) is stack
1639        True
1640        >>> stack.deepget((0,1))
1641        <PaintingLayer '01'>
1642        >>> stack.deepget((0,))
1643        <LayerStack len=3 '0'>
1644
1645        If the layer cannot be found, None is returned; however a
1646        different default value can be specified::
1647
1648        >>> stack.deepget((42,0), None)
1649        >>> stack.deepget((0,11), default="missing")
1650        'missing'
1651
1652        """
1653        if path is None:
1654            return default
1655        if len(path) == 0:
1656            return self
1657        unused_path = list(path)
1658        layer = self
1659        while len(unused_path) > 0:
1660            idx = unused_path.pop(0)
1661            if abs(idx) > (len(layer) - 1):
1662                return default
1663            layer = layer[idx]
1664            if unused_path:
1665                if not isinstance(layer, group.LayerStack):
1666                    return default
1667            else:
1668                return layer
1669        return default
1670
1671    def deepinsert(self, path, layer):
1672        """Inserts a layer before the final index in path
1673
1674        :param path: an insertion path: see below
1675        :type path: iterable of integers
1676        :param layer: the layer to insert
1677        :type layer: LayerBase
1678
1679        Deepinsert cannot create sub-stacks. Every element of `path`
1680        before the final element must be a valid `list`-style ``[]``
1681        index into an existing stack along the chain being addressed,
1682        starting with the root.  The final element may be any index
1683        which `list.insert()` accepts.  Negative final indices, and
1684        final indices greater than the number of layers in the addressed
1685        stack are quite valid in `path`::
1686
1687        >>> from . import data
1688        >>> from . import test
1689        >>> stack, leaves = test.make_test_stack()
1690        >>> layer = data.PaintingLayer(name='foo')
1691        >>> stack.deepinsert((0,9999), layer)
1692        >>> stack.deepget((0,-1)) is layer
1693        True
1694        >>> layer = data.PaintingLayer(name='foo')
1695        >>> stack.deepinsert([0], layer)
1696        >>> stack.deepget([0]) is layer
1697        True
1698
1699        Inserting a layer using this method gives it a unique name
1700        within the tree::
1701
1702        >>> layer.name != 'foo'
1703        True
1704        """
1705        if len(path) == 0:
1706            raise IndexError('Cannot insert after the root')
1707        unused_path = list(path)
1708        stack = self
1709        while len(unused_path) > 0:
1710            idx = unused_path.pop(0)
1711            if not isinstance(stack, group.LayerStack):
1712                raise IndexError("All nonfinal elements of %r must "
1713                                 "identify a stack" % (path,))
1714            if unused_path:
1715                stack = stack[idx]
1716            else:
1717                stack.insert(idx, layer)
1718                layer.name = self.get_unique_name(layer)
1719                self._propagate_symmetry_state(layer)
1720                return
1721        assert (len(unused_path) > 0), ("deepinsert() should never "
1722                                        "exhaust the path")
1723
1724    def deeppop(self, path):
1725        """Removes a layer by its path
1726
1727        >>> from . import test
1728        >>> stack, leaves = test.make_test_stack()
1729        >>> stack.deeppop(())
1730        Traceback (most recent call last):
1731        ...
1732        IndexError: Cannot pop the root stack
1733        >>> stack.deeppop([0])
1734        <LayerStack len=3 '0'>
1735        >>> stack.deeppop((0,1))
1736        <PaintingLayer '11'>
1737        >>> stack.deeppop((0,2))  # doctest: +ELLIPSIS
1738        Traceback (most recent call last):
1739        ...
1740        IndexError: ...
1741        """
1742        if len(path) == 0:
1743            raise IndexError("Cannot pop the root stack")
1744        parent_path = path[:-1]
1745        child_index = path[-1]
1746        if len(parent_path) == 0:
1747            parent = self
1748        else:
1749            parent = self.deepget(parent_path)
1750        old_current = self.current_path
1751        removed = parent.pop(child_index)
1752        self.current_path = old_current  # i.e. nearest remaining
1753        return removed
1754
1755    def deepremove(self, layer):
1756        """Removes a layer from any of the root's descendents
1757
1758        >>> from . import test
1759        >>> stack, leaves = test.make_test_stack()
1760        >>> stack.deepremove(leaves[3])
1761        >>> stack.deepremove(leaves[2])
1762        >>> stack.deepremove(stack.deepget([0]))
1763        >>> stack
1764        <RootLayerStack len=1>
1765        >>> stack.deepremove(leaves[3])
1766        Traceback (most recent call last):
1767        ...
1768        ValueError: Layer is not in the root stack or any descendent
1769        """
1770        if layer is self:
1771            raise ValueError("Cannot remove the root stack")
1772        old_current = self.current_path
1773        for path, descendent_layer in self.walk():
1774            assert len(path) > 0
1775            if descendent_layer is not layer:
1776                continue
1777            parent_path = path[:-1]
1778            if len(parent_path) == 0:
1779                parent = self
1780            else:
1781                parent = self.deepget(parent_path)
1782            parent.remove(layer)
1783            self.current_path = old_current  # i.e. nearest remaining
1784            return None
1785        raise ValueError("Layer is not in the root stack or "
1786                         "any descendent")
1787
1788    def deepindex(self, layer):
1789        """Return a path for a layer by searching the stack tree
1790
1791        >>> from . import test
1792        >>> stack, leaves = test.make_test_stack()
1793        >>> stack.deepindex(stack)
1794        ()
1795        >>> [stack.deepindex(l) for l in leaves]
1796        [(0, 0), (0, 1), (0, 2), (1, 0), (1, 1), (1, 2)]
1797        """
1798        if layer is self:
1799            return ()
1800        for path, ly in self.walk():
1801            if ly is layer:
1802                return tuple(path)
1803        return None
1804
1805    ## Convenience methods for commands
1806
1807    def canonpath(self, index=None, layer=None, path=None,
1808                  usecurrent=False, usefirst=False):
1809        """Verify and return the path for a layer from various criteria
1810
1811        :param index: index of the layer in walk() order
1812        :param layer: a layer, which must be a descendent of this root
1813        :param path: a layer path
1814        :param usecurrent: if true, use the current path as fallback
1815        :param usefirst: if true, use the first path as fallback
1816        :return: a new, verified path referring to an existing layer
1817        :rtype: tuple
1818
1819        The returned path is guaranteed to refer to an existing layer
1820        other than the root, and be the path in its most canonical
1821        form::
1822
1823          >>> root = RootLayerStack(doc=None)
1824          >>> root.deepinsert([0], data.PaintingLayer())
1825          >>> root.deepinsert([1], group.LayerStack())
1826          >>> root.deepinsert([1, 0], data.PaintingLayer())
1827          >>> layer = data.PaintingLayer()
1828          >>> root.deepinsert([1, 1], layer)
1829          >>> root.deepinsert([1, 2], data.PaintingLayer())
1830          >>> root.canonpath(layer=layer)
1831          (1, 1)
1832          >>> root.canonpath(path=(-1, -2))
1833          (1, 1)
1834          >>> root.canonpath(index=3)
1835          (1, 1)
1836
1837        Fallbacks can be specified for times when the regular criteria
1838        don't work::
1839
1840          >>> root.current_path = (1, 1)
1841          >>> root.canonpath(usecurrent=True)
1842          (1, 1)
1843          >>> root.canonpath(usefirst=True)
1844          (0,)
1845
1846        If no matching layer exists, a ValueError is raised::
1847
1848          >>> root.clear()
1849          >>> root.canonpath(usecurrent=True)
1850          ... # doctest: +ELLIPSIS
1851          Traceback (most recent call last):
1852          ...
1853          ValueError: ...
1854          >>> root.canonpath(usefirst=True)
1855          ... # doctest: +ELLIPSIS
1856          Traceback (most recent call last):
1857          ...
1858          ValueError: ...
1859        """
1860        if path is not None:
1861            layer = self.deepget(path)
1862            if layer is self:
1863                raise ValueError("path=%r is root: must be descendent" %
1864                                 (path,))
1865            if layer is not None:
1866                path = self.deepindex(layer)
1867                assert self.deepget(path) is layer
1868                return path
1869            elif not usecurrent:
1870                raise ValueError("layer not found with path=%r" %
1871                                 (path,))
1872        elif index is not None:
1873            if index < 0:
1874                raise ValueError("negative layer index %r" % (index,))
1875            for i, (path, layer) in enumerate(self.walk()):
1876                if i == index:
1877                    assert self.deepget(path) is layer
1878                    return path
1879            if not usecurrent:
1880                raise ValueError("layer not found with index=%r" %
1881                                 (index,))
1882        elif layer is not None:
1883            if layer is self:
1884                raise ValueError("layer is root stack: must be "
1885                                 "descendent")
1886            path = self.deepindex(layer)
1887            if path is not None:
1888                assert self.deepget(path) is layer
1889                return path
1890            elif not usecurrent:
1891                raise ValueError("layer=%r not found" % (layer,))
1892        # Criterion failed. Try fallbacks.
1893        if usecurrent:
1894            path = self.get_current_path()
1895            layer = self.deepget(path)
1896            if layer is not None:
1897                if layer is self:
1898                    raise ValueError("The current layer path refers to "
1899                                     "the root stack")
1900                path = self.deepindex(layer)
1901                assert self.deepget(path) is layer
1902                return path
1903            if not usefirst:
1904                raise ValueError("Invalid current path; usefirst "
1905                                 "might work but not specified")
1906        if usefirst:
1907            if len(self) > 0:
1908                path = (0,)
1909                assert self.deepget(path) is not None
1910                return path
1911            else:
1912                raise ValueError("Invalid current path; stack is empty")
1913        raise TypeError("No layer/index/path criterion, and "
1914                        "no fallback criteria")
1915
1916    ## Layer merging
1917
1918    def layer_new_normalized(self, path):
1919        """Copy a layer to a normal painting layer that looks the same
1920
1921        :param tuple path: Path to normalize
1922        :returns: New normalized layer
1923        :rtype: lib.layer.data.PaintingLayer
1924
1925        The normalize operation does whatever is needed to convert a
1926        layer of any type into a normal painting layer with full opacity
1927        and Normal combining mode, while retaining its appearance at the
1928        current time. This may mean:
1929
1930        * Just a simple copy
1931        * Merging all of its visible sublayers into the copy
1932        * Removing the effect the backdrop has on its appearance
1933
1934        The returned painting layer is not inserted into the tree
1935        structure, and nothing in the tree structure is changed by this
1936        operation. The layer returned is always fully opaque, visible,
1937        and has normal mode. Its strokemap is constructed from all
1938        visible and tangible painting layers in the original, and it has
1939        the same name as the original, initially.
1940
1941        >>> from . import test
1942        >>> root, leaves = test.make_test_stack()
1943        >>> orig_walk = list(root.walk())
1944        >>> orig_layers = {l for (p,l) in orig_walk}
1945        >>> for path, layer in orig_walk:
1946        ...     normized = root.layer_new_normalized(path)
1947        ...     assert normized not in orig_layers  # always a new layer
1948        >>> assert list(root.walk()) == orig_walk  # structure unchanged
1949
1950        """
1951        srclayer = self.deepget(path)
1952        if not srclayer:
1953            raise ValueError("Path %r not found", path)
1954
1955        # Simplest case
1956        if not srclayer.visible:
1957            return data.PaintingLayer(name=srclayer.name)
1958
1959        if isinstance(srclayer, data.PaintingLayer):
1960            if srclayer.mode == lib.mypaintlib.CombineSpectralWGM:
1961                return deepcopy(srclayer)
1962
1963        # Backdrops need removing if they combine with this layer's data.
1964        # Surface-backed layers' tiles can just be used as-is if they're
1965        # already fairly normal.
1966        needs_backdrop_removal = True
1967        if (srclayer.mode == lib.mypaintlib.CombineNormal
1968            and srclayer.opacity == 1.0):
1969
1970            # Optimizations for the tiled-surface types
1971            if isinstance(srclayer, data.PaintingLayer):
1972                return deepcopy(srclayer)  # include strokes
1973            elif isinstance(srclayer, data.SurfaceBackedLayer):
1974                return data.PaintingLayer.new_from_surface_backed_layer(
1975                    srclayer
1976                )
1977
1978            # Otherwise we're gonna have to render the source layer,
1979            # but we can skip the background removal *most* of the time.
1980            if isinstance(srclayer, group.LayerStack):
1981                needs_backdrop_removal = (srclayer.mode == PASS_THROUGH_MODE)
1982            else:
1983                needs_backdrop_removal = False
1984        # Begin building output, collecting tile indices and strokemaps.
1985        dstlayer = data.PaintingLayer()
1986        dstlayer.name = srclayer.name
1987        if srclayer.mode == lib.mypaintlib.CombineSpectralWGM:
1988            dstlayer.mode = srclayer.mode
1989        else:
1990            dstlayer.mode = lib.mypaintlib.CombineNormal
1991        tiles = set()
1992        for p, layer in self.walk():
1993            if not path_startswith(p, path):
1994                continue
1995            tiles.update(layer.get_tile_coords())
1996            if (isinstance(layer, data.PaintingLayer)
1997                    and not layer.locked
1998                    and not layer.branch_locked):
1999                dstlayer.strokes[:0] = layer.strokes
2000
2001        # Might need to render the backdrop, in order to subtract it.
2002        bd_ops = []
2003        if needs_backdrop_removal:
2004            bd_spec = self._get_backdrop_render_spec_for_layer(path)
2005            bd_ops = self.get_render_ops(bd_spec)
2006
2007        # Need to render the layer to be normalized too.
2008        # The ops are processed on top of the tiles bd_ops will render.
2009        src_spec = rendering.Spec(
2010            current=srclayer,
2011            solo=True,
2012            layers=set(self.layers_along_or_under_path(path))
2013        )
2014        src_ops = self.get_render_ops(src_spec)
2015
2016        # Process by tile.
2017        # This is like taking before/after pics from a normal render(),
2018        # then subtracting the before from the after.
2019        logger.debug("Normalize: bd_ops = %r", bd_ops)
2020        logger.debug("Normalize: src_ops = %r", src_ops)
2021        dstsurf = dstlayer._surface
2022        tiledims = (tiledsurface.N, tiledsurface.N, 4)
2023        for tx, ty in tiles:
2024            bd = np.zeros(tiledims, dtype='uint16')
2025            with dstsurf.tile_request(tx, ty, readonly=False) as dst:
2026                self._process_ops_list(bd_ops, bd, True, tx, ty, 0)
2027                lib.mypaintlib.tile_copy_rgba16_into_rgba16(bd, dst)
2028                self._process_ops_list(src_ops, dst, True, tx, ty, 0)
2029                if bd_ops:
2030                    dst[:, :, 3] = 0  # minimize alpha (discard original)
2031                    lib.mypaintlib.tile_flat2rgba(dst, bd)
2032
2033        return dstlayer
2034
2035    def get_merge_down_target(self, path):
2036        """Returns the target path for Merge Down, after checks
2037
2038        :param tuple path: Source path for the Merge Down
2039        :returns: Target path for the merge, if it exists
2040        :rtype: tuple (or None)
2041        """
2042        if not path:
2043            return None
2044
2045        source = self.deepget(path)
2046        if (source is None
2047                or source.locked
2048                or source.branch_locked
2049                or not source.get_mode_normalizable()):
2050            return None
2051
2052        target_path = path[:-1] + (path[-1] + 1,)
2053
2054        target = self.deepget(target_path)
2055        if (target is None
2056                or target.locked
2057                or target.branch_locked
2058                or not target.get_mode_normalizable()):
2059            return None
2060
2061        return target_path
2062
2063    def layer_new_merge_down(self, path):
2064        """Create a new layer containing the Merge Down of two layers
2065
2066        :param tuple path: Path to the top layer to Merge Down
2067        :returns: New merged layer
2068        :rtype: lib.layer.data.PaintingLayer
2069
2070        The current layer and the one below it are merged into a new
2071        layer, if that is possible, and the new layer is returned.
2072        Nothing is inserted or removed from the stack.  Any merged layer
2073        will contain a combined strokemap based on the input layers -
2074        although locked layers' strokemaps are not merged.
2075
2076        You get what you see. This means that both layers must be
2077        visible to be used in the output.
2078
2079        >>> from . import test
2080        >>> root, leaves = test.make_test_stack()
2081        >>> orig_walk = list(root.walk())
2082        >>> orig_layers = {l for (p,l) in orig_walk}
2083        >>> n_merged = 0
2084        >>> n_not_merged = 0
2085        >>> for path, layer in orig_walk:
2086        ...     try:
2087        ...         merged = root.layer_new_merge_down(path)
2088        ...     except ValueError:   # expect this
2089        ...         n_not_merged += 1
2090        ...         continue
2091        ...     assert merged not in orig_layers  # always a new layer
2092        ...     n_merged += 1
2093        >>> assert list(root.walk()) == orig_walk  # structure unchanged
2094        >>> assert n_merged > 0
2095        >>> assert n_not_merged > 0
2096
2097        """
2098        target_path = self.get_merge_down_target(path)
2099        if not target_path:
2100            raise ValueError("Invalid path for Merge Down")
2101        # Normalize input
2102        merge_layers = []
2103        for p in [target_path, path]:
2104            assert p is not None
2105            layer = self.layer_new_normalized(p)
2106            merge_layers.append(layer)
2107        assert None not in merge_layers
2108        # Build output strokemap, determine set of data tiles to merge
2109        dstlayer = data.PaintingLayer()
2110        srclayer = self.deepget(path)
2111        if srclayer.mode == lib.mypaintlib.CombineSpectralWGM:
2112            dstlayer.mode = srclayer.mode
2113        else:
2114            dstlayer.mode = lib.mypaintlib.CombineNormal
2115        tiles = set()
2116        for layer in merge_layers:
2117            tiles.update(layer.get_tile_coords())
2118            assert isinstance(layer, data.PaintingLayer)
2119            assert not layer.locked
2120            assert not layer.branch_locked
2121            dstlayer.strokes[:0] = layer.strokes
2122        # Build a (hopefully sensible) combined name too
2123        names = [l.name for l in reversed(merge_layers)
2124                 if l.has_interesting_name()]
2125        name = C_(
2126            "layer default names: joiner punctuation for merged layers",
2127            u", ",
2128        ).join(names)
2129        if name != '':
2130            dstlayer.name = name
2131        logger.debug("Merge Down: normalized source=%r", merge_layers)
2132        # Rendering loop
2133        dstsurf = dstlayer._surface
2134        for tx, ty in tiles:
2135            with dstsurf.tile_request(tx, ty, readonly=False) as dst:
2136                for layer in merge_layers:
2137                    mode = layer.mode
2138                    if mode != lib.mypaintlib.CombineSpectralWGM:
2139                        mode = lib.mypaintlib.CombineNormal
2140                    layer._surface.composite_tile(
2141                        dst, True,
2142                        tx, ty, mipmap_level=0,
2143                        mode=mode, opacity=layer.opacity
2144                    )
2145        return dstlayer
2146
2147    def layer_new_merge_visible(self):
2148        """Create and return the merge of all currently visible layers
2149
2150        :returns: New merged layer
2151        :rtype: lib.layer.data.PaintingLayer
2152
2153        All visible layers are merged into a new PaintingLayer, which is
2154        returned. Nothing is inserted or removed from the stack.  The
2155        merged layer will contain a combined strokemap based on those
2156        layers which are visible but not locked.
2157
2158        You get what you see. If the background layer is visible at the
2159        time of the merge, then many modes will pick up an image of it.
2160        It will be "subtracted" from the result of the merge so that the
2161        merge result can be stacked above the same background.
2162
2163        >>> from . import test
2164        >>> root, leaves = test.make_test_stack()
2165        >>> orig_walk = list(root.walk())
2166        >>> orig_layers = {l for (p,l) in orig_walk}
2167        >>> merged = root.layer_new_merge_visible()
2168        >>> assert list(root.walk()) == orig_walk  # structure unchanged
2169        >>> assert merged not in orig_layers   # layer is a new object
2170
2171        See also: `walk()`, `background_visible`.
2172        """
2173
2174        # Extract tile indices, names, and strokemaps.
2175        tiles = set()
2176        strokes = []
2177        names = []
2178        for path, layer in self.walk(visible=True):
2179            tiles.update(layer.get_tile_coords())
2180            if (isinstance(layer, data.StrokemappedPaintingLayer)
2181                    and not layer.locked
2182                    and not layer.branch_locked):
2183                strokes[:0] = layer.strokes
2184            if layer.has_interesting_name():
2185                names.append(layer.name)
2186
2187        # Start making the output layer.
2188        dstlayer = data.PaintingLayer()
2189        dstlayer.mode = lib.mypaintlib.CombineNormal
2190        dstlayer.strokes = strokes
2191        name = C_(
2192            "layer default names: joiner punctuation for merged layers",
2193            u", ",
2194        ).join(names)
2195        if name != '':
2196            dstlayer.name = name
2197        dstsurf = dstlayer._surface
2198
2199        # Render the entire tree, mostly normally.
2200        # Solo mode counts as normal, previewing mode does not.
2201        spec = self._get_render_spec(respect_previewing=False)
2202        self.render(dstsurf, tiles, 0, spec=spec)
2203
2204        # Then subtract the background surface if it was rendered.
2205        # This leaves a ghost image.
2206        # Sure, we could render isolated for the case where all layers
2207        # that hit the background composite with src-over.
2208        # But that makes an exception and Exceptions Are Bad™.
2209        # Especially if they're really non-obvious to the user, like this.
2210        # Maybe it'd be better to split this op into two variants,
2211        # "Remove Background" and "Ignore Background"?
2212        if self._get_render_background(spec):
2213            bgsurf = self._background_layer._surface
2214            for tx, ty in tiles:
2215                with dstsurf.tile_request(tx, ty, readonly=False) as dst:
2216                    with bgsurf.tile_request(tx, ty, readonly=True) as bg:
2217                        dst[:, :, 3] = 0  # minimize alpha (discard original)
2218                        lib.mypaintlib.tile_flat2rgba(dst, bg)
2219
2220        return dstlayer
2221
2222    ## Layer uniquifying (sort of the opposite of Merge Down)
2223
2224    def uniq_layer(self, path, pixels=False):
2225        """Uniquify a painting layer's tiles or pixels."""
2226        targ_path = path
2227        targ_layer = self.deepget(path)
2228
2229        if targ_layer is None:
2230            logger.error("uniq: target layer not found")
2231            return
2232        if not isinstance(targ_layer, data.PaintingLayer):
2233            logger.error("uniq: target layer is not a painting layer")
2234            return
2235
2236        # Extract ops lists for the target and its backdrop
2237        bd_spec = self._get_backdrop_render_spec_for_layer(targ_path)
2238        bd_ops = self.get_render_ops(bd_spec)
2239
2240        targ_only_spec = rendering.Spec(
2241            current=targ_layer,
2242            solo=True,
2243            layers=set(self.layers_along_or_under_path(targ_path))
2244        )
2245        targ_only_ops = self.get_render_ops(targ_only_spec)
2246
2247        # Process by tile, like Normalize's backdrop removal.
2248        logger.debug("uniq: bd_ops = %r", bd_ops)
2249        logger.debug("uniq: targ_only_ops = %r", targ_only_ops)
2250        targ_surf = targ_layer._surface
2251        tile_dims = (tiledsurface.N, tiledsurface.N, 4)
2252        unchanged_tile_indices = set()
2253        zeros = np.zeros(tile_dims, dtype='uint16')
2254        for tx, ty in targ_surf.get_tiles():
2255            bd_img = copy(zeros)
2256            self._process_ops_list(bd_ops, bd_img, True, tx, ty, 0)
2257            targ_img = copy(bd_img)
2258            self._process_ops_list(targ_only_ops, targ_img, True, tx, ty, 0)
2259            equal_channels = (targ_img == bd_img)   # NxNn4 dtype=bool
2260            if equal_channels.all():
2261                unchanged_tile_indices.add((tx, ty))
2262            elif pixels:
2263                equal_px = equal_channels.all(axis=2, keepdims=True)  # NxNx1
2264                with targ_surf.tile_request(tx, ty, readonly=False) as targ:
2265                    targ *= np.invert(equal_px)
2266
2267        targ_surf.remove_tiles(unchanged_tile_indices)
2268
2269    def refactor_layer_group(self, path, pixels=False):
2270        """Factor common stuff out of a group's child layers."""
2271        targ_path = path
2272        targ_group = self.deepget(path)
2273
2274        if targ_group is None:
2275            logger.error("refactor: target group not found")
2276            return
2277        if not isinstance(targ_group, group.LayerStack):
2278            logger.error("refactor: target group is not a LayerStack")
2279            return
2280        if targ_group.mode == PASS_THROUGH_MODE:
2281            logger.error("refactor: target group is not isolated")
2282            return
2283        if len(targ_group) == 0:
2284            return
2285
2286        # Normalize each child layer that needs it.
2287        # Refactoring can cope with some opacity variations.
2288        for i, child in enumerate(targ_group):
2289            if child.mode == lib.mypaintlib.CombineNormal:
2290                continue
2291            child_path = tuple(list(targ_path) + [i])
2292            child = self.layer_new_normalized(child_path)
2293            targ_group[i] = child
2294
2295        # Extract ops list fragments for the child layers.
2296        normalized_child_layers = list(targ_group)
2297        child_ops = {}
2298        union_tiles = set()
2299        for i, child in enumerate(normalized_child_layers):
2300            child_path = tuple(list(targ_path) + [i])
2301            spec = rendering.Spec(
2302                current=child,
2303                solo=True,
2304                layers=set(self.layers_along_or_under_path(child_path))
2305            )
2306            ops = self.get_render_ops(spec)
2307            child_ops[child] = ops
2308            union_tiles.update(child.get_tile_coords())
2309
2310        # Insert a layer to contain all the common pixels or tiles
2311        common_layer = data.PaintingLayer()
2312        common_layer.mode = lib.mypaintlib.CombineNormal
2313        common_layer.name = C_(
2314            "layer default names: refactor: name of the common areas layer",
2315            u"Common",
2316        )
2317        common_surf = common_layer._surface
2318        targ_group.append(common_layer)
2319
2320        # Process by tile
2321        n = tiledsurface.N
2322        zeros_rgba = np.zeros((n, n, 4), dtype='uint16')
2323        ones_bool = np.ones((n, n, 1), dtype='bool')
2324        common_data_tiles = set()
2325        child0 = normalized_child_layers[0]
2326        child0_surf = child0._surface
2327        for tx, ty in union_tiles:
2328            common_px = copy(ones_bool)
2329            rgba0 = None
2330            for child in normalized_child_layers:
2331                ops = child_ops[child]
2332                rgba = copy(zeros_rgba)
2333                self._process_ops_list(ops, rgba, True, tx, ty, 0)
2334                if rgba0 is None:
2335                    rgba0 = rgba
2336                else:
2337                    common_px &= (rgba0 == rgba).all(axis=2, keepdims=True)
2338
2339            if common_px.all():
2340                with common_surf.tile_request(tx, ty, readonly=False) as d:
2341                    with child0_surf.tile_request(tx, ty, readonly=True) as s:
2342                        d[:] = s
2343                common_data_tiles.add((tx, ty))
2344
2345            elif pixels and common_px.any():
2346                with common_surf.tile_request(tx, ty, readonly=False) as d:
2347                    with child0_surf.tile_request(tx, ty, readonly=True) as s:
2348                        d[:] = s * common_px
2349                for child in normalized_child_layers:
2350                    surf = child._surface
2351                    if (tx, ty) in surf.get_tiles():
2352                        with surf.tile_request(tx, ty, readonly=False) as d:
2353                            d *= np.invert(common_px)
2354
2355        # Remove the remaining complete common tiles.
2356        for child in normalized_child_layers:
2357            surf = child._surface
2358            surf.remove_tiles(common_data_tiles)
2359
2360    ## Loading
2361
2362    def load_from_openraster(self, orazip, elem, cache_dir, progress,
2363                             x=0, y=0, **kwargs):
2364        """Load the root layer stack from an open .ora file
2365
2366        >>> root = RootLayerStack(None)
2367        >>> import zipfile
2368        >>> import tempfile
2369        >>> import xml.etree.ElementTree as ET
2370        >>> import shutil
2371        >>> tmpdir = tempfile.mkdtemp()
2372        >>> assert os.path.exists(tmpdir)
2373        >>> with zipfile.ZipFile("tests/bigimage.ora") as orazip:
2374        ...     image_elem = ET.fromstring(orazip.read("stack.xml"))
2375        ...     stack_elem = image_elem.find("stack")
2376        ...     root.load_from_openraster(
2377        ...         orazip=orazip,
2378        ...         elem=stack_elem,
2379        ...         cache_dir=tmpdir,
2380        ...         progress=None,
2381        ...     )
2382        >>> len(list(root.walk())) > 0
2383        True
2384        >>> shutil.rmtree(tmpdir)
2385        >>> assert not os.path.exists(tmpdir)
2386
2387        """
2388        self._no_background = True
2389        super(RootLayerStack, self).load_from_openraster(
2390            orazip,
2391            elem,
2392            cache_dir,
2393            progress,
2394            x=x, y=y,
2395            **kwargs
2396        )
2397        del self._no_background
2398        self._set_current_path_after_ora_load()
2399        self._mark_all_layers_for_rethumb()
2400
2401    def _set_current_path_after_ora_load(self):
2402        """Set a suitable working layer after loading from oradir/orazip"""
2403        # Select a suitable working layer from the user-accesible ones.
2404        # Try for the uppermost layer marked as initially selected,
2405        # fall back to the uppermost immediate child of the root stack.
2406        num_loaded = 0
2407        selected_path = None
2408        uppermost_child_path = None
2409        for path, loaded_layer in self.walk():
2410            if not selected_path and loaded_layer.initially_selected:
2411                selected_path = path
2412            if not uppermost_child_path and len(path) == 1:
2413                uppermost_child_path = path
2414            num_loaded += 1
2415        logger.debug("Loaded %d layer(s)" % num_loaded)
2416        num_layers = num_loaded
2417        if num_loaded == 0:
2418            logger.error('Could not load any layer, document is empty.')
2419            if self.doc and self.doc.CREATE_PAINTING_LAYER_IF_EMPTY:
2420                logger.info('Adding an empty painting layer')
2421                self.ensure_populated()
2422                selected_path = [0]
2423                num_layers = len(self)
2424                assert num_layers > 0
2425            else:
2426                logger.warning("No layers, and doc debugging flag is active")
2427                return
2428        if not selected_path:
2429            selected_path = uppermost_child_path
2430        selected_path = tuple(selected_path)
2431        logger.debug("Selecting %r after load", selected_path)
2432        self.set_current_path(selected_path)
2433
2434    def _load_child_layer_from_orazip(self, orazip, elem, cache_dir,
2435                                      progress, x=0, y=0, **kwargs):
2436        """Loads and appends a single child layer from an open .ora file"""
2437        attrs = elem.attrib
2438        # Handle MyPaint's special background tile notation
2439        # MyPaint will support reading .ora files using the legacy
2440        # background tile attribute until v2.0.0.
2441        bg_src_attrs = [
2442            data.BackgroundLayer.ORA_BGTILE_ATTR,
2443            data.BackgroundLayer.ORA_BGTILE_LEGACY_ATTR,
2444        ]
2445        for bg_src_attr in bg_src_attrs:
2446            bg_src = attrs.get(bg_src_attr, None)
2447            if not bg_src:
2448                continue
2449            logger.debug(
2450                "Found bg tile %r in %r",
2451                bg_src,
2452                bg_src_attr,
2453            )
2454            assert self._no_background, "Only one background is permitted"
2455            try:
2456                bg_pixbuf = lib.pixbuf.load_from_zipfile(
2457                    datazip=orazip,
2458                    filename=bg_src,
2459                    progress=progress,
2460                )
2461                self.set_background(bg_pixbuf)
2462                self._no_background = False
2463                return
2464            except tiledsurface.BackgroundError as e:
2465                logger.warning('ORA background tile not usable: %r', e)
2466        super(RootLayerStack, self)._load_child_layer_from_orazip(
2467            orazip,
2468            elem,
2469            cache_dir,
2470            progress,
2471            x=x, y=y,
2472            **kwargs
2473        )
2474
2475    def load_from_openraster_dir(self, oradir, elem, cache_dir, progress,
2476                                 x=0, y=0, **kwargs):
2477        """Loads layer flags and data from an OpenRaster-style dir"""
2478        self._no_background = True
2479        super(RootLayerStack, self).load_from_openraster_dir(
2480            oradir,
2481            elem,
2482            cache_dir,
2483            progress,
2484            x=x, y=y,
2485            **kwargs
2486        )
2487        del self._no_background
2488        self._set_current_path_after_ora_load()
2489        self._mark_all_layers_for_rethumb()
2490
2491    def _load_child_layer_from_oradir(self, oradir, elem, cache_dir,
2492                                      progress, x=0, y=0, **kwargs):
2493        """Loads and appends a single child layer from an open .ora file"""
2494        attrs = elem.attrib
2495        # Handle MyPaint's special background tile notation
2496        # MyPaint will support reading .ora files using the legacy
2497        # background tile attribute until v2.0.0.
2498        bg_src_attrs = [
2499            data.BackgroundLayer.ORA_BGTILE_ATTR,
2500            data.BackgroundLayer.ORA_BGTILE_LEGACY_ATTR,
2501        ]
2502        for bg_src_attr in bg_src_attrs:
2503            bg_src = attrs.get(bg_src_attr, None)
2504            if not bg_src:
2505                continue
2506            logger.debug(
2507                "Found bg tile %r in %r",
2508                bg_src,
2509                bg_src_attr,
2510            )
2511            assert self._no_background, "Only one background is permitted"
2512            try:
2513                bg_pixbuf = lib.pixbuf.load_from_file(
2514                    filename = os.path.join(oradir, bg_src),
2515                    progress = progress,
2516                )
2517                self.set_background(bg_pixbuf)
2518                self._no_background = False
2519                return
2520            except tiledsurface.BackgroundError as e:
2521                logger.warning('ORA background tile not usable: %r', e)
2522        super(RootLayerStack, self)._load_child_layer_from_oradir(
2523            oradir,
2524            elem,
2525            cache_dir,
2526            progress,
2527            x=x, y=y,
2528            **kwargs
2529        )
2530
2531    ## Saving
2532
2533    def save_to_openraster(self, orazip, tmpdir, path, canvas_bbox,
2534                           frame_bbox, progress=None, **kwargs):
2535        """Saves the stack's data into an open OpenRaster ZipFile"""
2536        if not progress:
2537            progress = lib.feedback.Progress()
2538        progress.items = 10
2539
2540        # First 90%: save the stack contents normally.
2541        stack_elem = super(RootLayerStack, self).save_to_openraster(
2542            orazip, tmpdir, path, canvas_bbox,
2543            frame_bbox,
2544            progress=progress.open(9),
2545            **kwargs
2546        )
2547
2548        # Remaining 10%: save the special background layer too.
2549        bg_layer = self.background_layer
2550        bg_layer.initially_selected = False
2551        bg_path = (len(self),)
2552        bg_elem = bg_layer.save_to_openraster(
2553            orazip, tmpdir, bg_path,
2554            canvas_bbox, frame_bbox,
2555            progress=progress.open(1),
2556            **kwargs
2557        )
2558        stack_elem.append(bg_elem)
2559
2560        progress.close()
2561        return stack_elem
2562
2563    def queue_autosave(self, oradir, taskproc, manifest, bbox, **kwargs):
2564        """Queues the layer for auto-saving"""
2565        stack_elem = super(RootLayerStack, self).queue_autosave(
2566            oradir, taskproc, manifest, bbox,
2567            **kwargs
2568        )
2569        # Queue background layer
2570        bg_layer = self.background_layer
2571        bg_elem = bg_layer.queue_autosave(
2572            oradir, taskproc, manifest, bbox,
2573            **kwargs
2574        )
2575        stack_elem.append(bg_elem)
2576        return stack_elem
2577
2578    ## Notification mechanisms
2579
2580    @event
2581    def layer_content_changed(self, *args):
2582        """Event: notifies that sub-layer's pixels have changed"""
2583
2584    def _notify_layer_properties_changed(self, layer, changed):
2585        if layer is self:
2586            return
2587        assert layer.root is self
2588        path = self.deepindex(layer)
2589        assert path is not None, "Unable to find layer which was changed"
2590        self.layer_properties_changed(path, layer, changed)
2591
2592    @event
2593    def layer_properties_changed(self, path, layer, changed):
2594        """Event: notifies that a sub-layer's properties have changed"""
2595
2596    def _notify_layer_deleted(self, parent, oldchild, oldindex):
2597        assert parent.root is self
2598        assert oldchild.root is not self
2599        path = self.deepindex(parent)
2600        if path is None:  # e.g. layers within current_layer_overlay
2601            return
2602        path = path + (oldindex,)
2603        self.layer_deleted(path)
2604
2605    @event
2606    def layer_deleted(self, path):
2607        """Event: notifies that a sub-layer has been deleted"""
2608
2609    def _notify_layer_inserted(self, parent, newchild, newindex):
2610        assert parent.root is self
2611        assert newchild.root is self
2612        path = self.deepindex(newchild)
2613        if path is None:  # e.g. layers within current_layer_overlay
2614            return
2615        assert len(path) > 0
2616        self.layer_inserted(path)
2617
2618    @event
2619    def layer_inserted(self, path):
2620        """Event: notifies that a sub-layer has been added"""
2621        pass
2622
2623    @event
2624    def current_path_updated(self, path):
2625        """Event: notifies that the layer selection has been updated"""
2626        pass
2627
2628    def save_snapshot(self):
2629        """Snapshots the state of the layer, for undo purposes"""
2630        return RootLayerStackSnapshot(self)
2631
2632    ## Layer preview thumbnails
2633
2634    def _mark_all_layers_for_rethumb(self):
2635        self._rethumb_layers[:] = []
2636        for path, layer in self.walk():
2637            self._rethumb_layers.append(layer)
2638        self._restart_rethumb_timer()
2639
2640    def _mark_layer_for_rethumb(self, root, layer, *_ignored):
2641        if layer not in self._rethumb_layers:
2642            self._rethumb_layers.append(layer)
2643        self._restart_rethumb_timer()
2644
2645    def _restart_rethumb_timer(self):
2646        timer_id = self._rethumb_layers_timer_id
2647        if timer_id is not None:
2648            GLib.source_remove(timer_id)
2649        timer_id = GLib.timeout_add(
2650            priority=GLib.PRIORITY_LOW,
2651            interval=100,
2652            function=self._rethumb_layers_timer_cb,
2653        )
2654        self._rethumb_layers_timer_id = timer_id
2655
2656    def _rethumb_layers_timer_cb(self):
2657        if len(self._rethumb_layers) >= 1:
2658            layer0 = self._rethumb_layers.pop(-1)
2659            path0 = self.deepindex(layer0)
2660            if not path0:
2661                return True
2662            layer0.update_thumbnail()
2663            self.layer_thumbnail_updated(path0, layer0)
2664            # Queue parent layers too
2665            path = path0[:-1]
2666            parents = []
2667            while len(path) > 0:
2668                layer = self.deepget(path)
2669                if layer not in self._rethumb_layers:
2670                    parents.append(layer)
2671                path = path[:-1]
2672            self._rethumb_layers.extend(reversed(parents))
2673            return True
2674        # Stop the timer when there is nothing more to be done.
2675        self._rethumb_layers_timer_id = None
2676        return False
2677
2678    @event
2679    def layer_thumbnail_updated(self, path, layer):
2680        """Event: a layer thumbnail was updated.
2681
2682        :param tuple path: The path to _layer_.
2683        :param lib.layer.core.LayerBase layer: The layer that was updated.
2684
2685        See lib.layer.core.LayerBase.thumbnail
2686
2687        """
2688        pass
2689
2690
2691class RootLayerStackSnapshot (group.LayerStackSnapshot):
2692    """Snapshot of a root layer stack's state"""
2693
2694    def __init__(self, layer):
2695        super(RootLayerStackSnapshot, self).__init__(layer)
2696        self.bg_sshot = layer.background_layer.save_snapshot()
2697        self.bg_visible = layer.background_visible
2698        self.current_path = layer.current_path
2699
2700    def restore_to_layer(self, layer):
2701        super(RootLayerStackSnapshot, self).restore_to_layer(layer)
2702        layer.background_layer.load_snapshot(self.bg_sshot)
2703        layer.background_visible = self.bg_visible
2704        layer.current_path = self.current_path
2705
2706
2707class _TileRenderWrapper (TileAccessible, TileBlittable):
2708    """Adapts a RootLayerStack to support RO tile_request()s.
2709
2710    The wrapping is very minimal.
2711    Tiles are rendered into empty buffers on demand and cached.
2712    The tile request interface is therefore read only,
2713    and these wrappers should be used only as temporary objects.
2714
2715    """
2716
2717    def __init__(self, root, spec, use_cache=True):
2718        """Adapt a renderable object to support "tile_request()".
2719
2720        :param RootLayerStack root: root of a tree.
2721        :param lib.layer.rendering.Spec spec: How to render it.
2722        :param bool use_cache: Cache rendered output.
2723
2724        """
2725        super(_TileRenderWrapper, self).__init__()
2726        self._root = root
2727        self._spec = spec
2728        self._ops = root.get_render_ops(spec)
2729        self._use_cache = bool(use_cache)
2730        self._cache = {}
2731
2732        # Store the subset of layers that are visible, as a list.
2733        # If this is a solo layer, only filter from its sub-hierarchy.
2734        if spec.solo:
2735            self._visible_layers = spec.layers
2736        else:
2737            self._visible_layers = list(root.deepiter(visible=True))
2738
2739    @contextlib.contextmanager
2740    def tile_request(self, tx, ty, readonly):
2741        """Context manager that fetches a single tile as fix15 RGBA data.
2742
2743        :param int tx: Location to access (X coordinate).
2744        :param int ty: Location to access (Y coordinate).
2745        :param bool readonly: Must be True.
2746        :yields: One NumPy tile array.
2747
2748        To be used with the 'with' statement.
2749
2750        """
2751        if not readonly:
2752            raise ValueError("Only readonly tile requests are supported")
2753        dst = None
2754        if self._use_cache:
2755            dst = self._cache.get((tx, ty), None)
2756        if dst is None:
2757            bg_hidden = not self._root.root.background_visible
2758            if (self._spec.solo or bg_hidden) and self._all_empty(tx, ty):
2759                dst = tiledsurface.transparent_tile.rgba
2760            else:
2761                tiledims = (tiledsurface.N, tiledsurface.N, 4)
2762                dst = np.zeros(tiledims, 'uint16')
2763                self._root.render_single_tile(
2764                    dst, True,
2765                    tx, ty, 0,
2766                    ops=self._ops,
2767                )
2768            if self._use_cache:
2769                self._cache[(tx, ty)] = dst
2770        yield dst
2771
2772    def _all_empty(self, tx, ty):
2773        """Check that no tile exists at (tx, ty) in any visible layer"""
2774        tc = (tx, ty)
2775        for layer in self._visible_layers:
2776            if tc in layer.get_tile_coords():
2777                return False
2778        return True
2779
2780    def get_bbox(self):
2781        """Explicit passthrough of get_bbox"""
2782        return self._root.get_bbox()
2783
2784    def blit_tile_into(self, dst, dst_has_alpha, tx, ty, **kwargs):
2785        """Copy a rendered tile into a fix15 or 8bpp array."""
2786        assert dst.dtype == 'uint8'
2787        with self.tile_request(tx, ty, readonly=True) as src:
2788            assert src.dtype == 'uint16'
2789            if dst_has_alpha:
2790                conv = lib.mypaintlib.tile_convert_rgba16_to_rgba8
2791            else:
2792                conv = lib.mypaintlib.tile_convert_rgbu16_to_rgbu8
2793            conv(src, dst, eotf())
2794
2795    def __getattr__(self, attr):
2796        """Pass through calls to other methods"""
2797        return getattr(self._root, attr)
2798
2799
2800## Layer path tuple functions
2801
2802
2803def path_startswith(path, prefix):
2804    """Returns whether one path starts with another
2805
2806    :param tuple path: Path to be tested
2807    :param tuple prefix: Prefix path to be tested against
2808
2809    >>> path_startswith((1,2,3), (1,2))
2810    True
2811    >>> path_startswith((1,2,3), (1,2,3,4))
2812    False
2813    >>> path_startswith((1,2,3), (1,0))
2814    False
2815    """
2816    if len(prefix) > len(path):
2817        return False
2818    for i in xrange(len(prefix)):
2819        if path[i] != prefix[i]:
2820            return False
2821    return True
2822
2823
2824## Module testing
2825
2826
2827def _test():
2828    """Run doctest strings"""
2829    import doctest
2830    doctest.testmod(optionflags=doctest.ELLIPSIS)
2831
2832
2833if __name__ == '__main__':
2834    logging.basicConfig(level=logging.DEBUG)
2835    _test()
2836