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