1# This file is part of MyPaint. 2# -*- coding: utf-8 -*- 3# Copyright (C) 2019 by The Mypaint Development Team 4# Copyright (C) 2011-2017 by Andrew Chadwick <a.t.chadwick@gmail.com> 5# Copyright (C) 2007-2012 by Martin Renold <martinxyz@gmx.ch> 6# 7# This program is free software; you can redistribute it and/or modify 8# it under the terms of the GNU General Public License as published by 9# the Free Software Foundation; either version 2 of the License, or 10# (at your option) any later version. 11 12"""Data layer classes""" 13 14 15## Imports 16from __future__ import division, print_function 17 18import zlib 19import logging 20import os 21import time 22import tempfile 23import shutil 24from copy import deepcopy 25from random import randint 26import uuid 27import struct 28import contextlib 29 30from lib.brush import BrushInfo 31from lib.gettext import C_ 32from lib.tiledsurface import N 33import lib.tiledsurface as tiledsurface 34import lib.strokemap 35import lib.helpers as helpers 36import lib.fileutils 37import lib.pixbuf 38import lib.modes 39import lib.mypaintlib 40from . import core 41import lib.layer.error 42import lib.autosave 43import lib.xml 44import lib.feedback 45from . import rendering 46from lib.pycompat import PY3 47from lib.pycompat import unicode 48 49if PY3: 50 from io import StringIO 51 from io import BytesIO 52else: 53 from cStringIO import StringIO 54 55 56logger = logging.getLogger(__name__) 57 58 59## Base classes 60 61 62class SurfaceBackedLayer (core.LayerBase, lib.autosave.Autosaveable): 63 """Minimal Surface-backed layer implementation 64 65 This minimal implementation is backed by a surface, which is used 66 for rendering by by the main application; subclasses are free to 67 choose whether they consider the surface to be the canonical source 68 of layer data or something else with the surface being just a 69 preview. 70 """ 71 72 #: Suffixes allowed in load_from_openraster(). 73 #: Values are strings with leading dots. 74 #: Use a list containing "" to allow *any* file to be loaded. 75 #: The first item in the list can be used as a default extension. 76 ALLOWED_SUFFIXES = [] 77 78 #: Substitute content if the layer cannot be loaded. 79 FALLBACK_CONTENT = None 80 81 ## Initialization 82 83 def __init__(self, surface=None, **kwargs): 84 """Construct a new SurfaceBackedLayer 85 86 :param surface: Surface to use, overriding the default. 87 :param **kwargs: passed to superclass. 88 89 If `surface` is specified, content observers will not be attached, and 90 the layer will not be cleared during construction. The default is to 91 instantiate and use a new, observed, `tiledsurface.Surface`. 92 """ 93 super(SurfaceBackedLayer, self).__init__(**kwargs) 94 95 # Pluggable surface implementation 96 # Only connect observers if using the default tiled surface 97 if surface is None: 98 self._surface = tiledsurface.Surface() 99 self._surface.observers.append(self._content_changed) 100 else: 101 self._surface = surface 102 103 @classmethod 104 def new_from_surface_backed_layer(cls, src): 105 """Clone from another SurfaceBackedLayer 106 107 :param cls: Called as a @classmethod 108 :param SurfaceBackedLayer src: Source layer 109 :return: A new instance of type `cls`. 110 111 """ 112 if not isinstance(src, SurfaceBackedLayer): 113 raise ValueError("Source must be a SurfaceBacedLayer") 114 layer = cls() 115 src_snap = src.save_snapshot() 116 assert isinstance(src_snap, SurfaceBackedLayerSnapshot) 117 SurfaceBackedLayerSnapshot.restore_to_layer(src_snap, layer) 118 return layer 119 120 def load_from_surface(self, surface): 121 """Load the backing surface image's tiles from another surface""" 122 self._surface.load_from_surface(surface) 123 124 def load_from_strokeshape(self, strokeshape, bbox=None, center=None): 125 """Load image tiles from a stroke shape object. 126 127 :param strokemap.StrokeShape strokeshape: source shape 128 :param tuple bbox: Optional (x,y,w,h) pixel bbox to render in. 129 :param tuple center: Optional (x,y) center of interest. 130 131 """ 132 strokeshape.render_to_surface(self._surface, bbox=bbox, center=center) 133 134 ## Loading 135 136 def load_from_openraster(self, orazip, elem, cache_dir, progress, 137 x=0, y=0, **kwargs): 138 """Loads layer flags and bitmap/surface data from a .ora zipfile 139 140 The normal behaviour is to load the surface data directly from 141 the OpenRaster zipfile without using a temporary file. This 142 method also checks the src attribute's suffix against 143 ALLOWED_SUFFIXES before attempting to load the surface. 144 145 See: _load_surface_from_orazip_member() 146 147 """ 148 # Load layer flags 149 super(SurfaceBackedLayer, self).load_from_openraster( 150 orazip, 151 elem, 152 cache_dir, 153 progress, 154 x=x, y=y, 155 **kwargs 156 ) 157 # Read bitmap content into the surface 158 attrs = elem.attrib 159 src = attrs.get("src", None) 160 src_rootname, src_ext = os.path.splitext(src) 161 src_rootname = os.path.basename(src_rootname) 162 src_ext = src_ext.lower() 163 x += int(attrs.get('x', 0)) 164 y += int(attrs.get('y', 0)) 165 logger.debug( 166 "Trying to load %r at %+d%+d, as %r", 167 src, 168 x, y, 169 self.__class__.__name__, 170 ) 171 suffixes = self.ALLOWED_SUFFIXES 172 if ("" not in suffixes) and (src_ext not in suffixes): 173 logger.debug( 174 "Abandoning load attempt, cannot load %rs from a %r " 175 "(supported file extensions: %r)", 176 self.__class__.__name__, 177 src_ext, 178 suffixes, 179 ) 180 raise lib.layer.error.LoadingFailed( 181 "Only %r are supported" % (suffixes,), 182 ) 183 # Delegate the actual loading part 184 self._load_surface_from_orazip_member( 185 orazip, 186 cache_dir, 187 src, 188 progress, 189 x, y, 190 ) 191 192 def _load_surface_from_orazip_member(self, orazip, cache_dir, 193 src, progress, x, y): 194 """Loads the surface from a member of an OpenRaster zipfile 195 196 Intended strictly for override by subclasses which need to first 197 extract and then keep the file around afterwards. 198 199 """ 200 pixbuf = lib.pixbuf.load_from_zipfile( 201 datazip=orazip, 202 filename=src, 203 progress=progress, 204 ) 205 self.load_surface_from_pixbuf(pixbuf, x=x, y=y) 206 207 def load_from_openraster_dir(self, oradir, elem, cache_dir, progress, 208 x=0, y=0, **kwargs): 209 """Loads layer flags and data from an OpenRaster-style dir""" 210 # Load layer flags 211 super(SurfaceBackedLayer, self).load_from_openraster_dir( 212 oradir, 213 elem, 214 cache_dir, 215 progress, 216 x=x, y=y, 217 **kwargs 218 ) 219 # Read bitmap content into the surface 220 attrs = elem.attrib 221 src = attrs.get("src", None) 222 src_rootname, src_ext = os.path.splitext(src) 223 src_rootname = os.path.basename(src_rootname) 224 src_ext = src_ext.lower() 225 x += int(attrs.get('x', 0)) 226 y += int(attrs.get('y', 0)) 227 logger.debug( 228 "Trying to load %r at %+d%+d, as %r", 229 src, 230 x, y, 231 self.__class__.__name__, 232 ) 233 suffixes = self.ALLOWED_SUFFIXES 234 if ("" not in suffixes) and (src_ext not in suffixes): 235 logger.debug( 236 "Abandoning load attempt, cannot load %rs from a %r " 237 "(supported file extensions: %r)", 238 self.__class__.__name__, 239 src_ext, 240 suffixes, 241 ) 242 raise lib.layer.error.LoadingFailed( 243 "Only %r are supported" % (suffixes,), 244 ) 245 # Delegate the actual loading part 246 self._load_surface_from_oradir_member( 247 oradir, 248 cache_dir, 249 src, 250 progress, 251 x, y, 252 ) 253 254 def _load_surface_from_oradir_member(self, oradir, cache_dir, 255 src, progress, x, y): 256 """Loads the surface from a file in an OpenRaster-like folder 257 258 Intended strictly for override by subclasses which need to 259 make copies to manage. 260 261 """ 262 self.load_surface_from_pixbuf_file( 263 os.path.join(oradir, src), 264 x, y, 265 progress, 266 ) 267 268 def load_surface_from_pixbuf_file(self, filename, x=0, y=0, 269 progress=None): 270 """Loads the layer's surface from any file which GdkPixbuf can open""" 271 if progress: 272 if progress.items is not None: 273 raise ValueError( 274 "load_surface_from_pixbuf_file() expects " 275 "unsized progress objects" 276 ) 277 s = os.stat(filename) 278 progress.items = int(s.st_size) 279 try: 280 with open(filename, 'rb') as fp: 281 pixbuf = lib.pixbuf.load_from_stream(fp, progress) 282 except Exception as err: 283 if self.FALLBACK_CONTENT is None: 284 raise lib.layer.error.LoadingFailed( 285 "Failed to load %r: %r" % (filename, str(err)), 286 ) 287 logger.warning("Failed to load %r: %r", filename, str(err)) 288 logger.info("Using fallback content instead of %r", filename) 289 pixbuf = lib.pixbuf.load_from_stream( 290 StringIO(self.FALLBACK_CONTENT), 291 ) 292 return self.load_surface_from_pixbuf(pixbuf, x, y) 293 294 def load_surface_from_pixbuf(self, pixbuf, x=0, y=0): 295 """Loads the layer's surface from a GdkPixbuf""" 296 arr = helpers.gdkpixbuf2numpy(pixbuf) 297 surface = tiledsurface.Surface() 298 bbox = surface.load_from_numpy(arr, x, y) 299 self.load_from_surface(surface) 300 return bbox 301 302 def clear(self): 303 """Clears the layer""" 304 self._surface.clear() 305 306 ## Info methods 307 308 @property 309 def effective_opacity(self): 310 """The opacity used when compositing a layer: zero if invisible""" 311 if self.visible: 312 return self.opacity 313 else: 314 return 0.0 315 316 def get_alpha(self, x, y, radius): 317 """Gets the average alpha within a certain radius at a point""" 318 return self._surface.get_alpha(x, y, radius) 319 320 def get_bbox(self): 321 """Returns the inherent bounding box of the surface, tile aligned""" 322 return self._surface.get_bbox() 323 324 def is_empty(self): 325 """Tests whether the surface is empty""" 326 return self._surface.is_empty() 327 328 ## Flood fill 329 330 def flood_fill(self, fill_args, dst_layer=None): 331 """Fills a point on the surface with a color 332 333 See `PaintingLayer.flood_fill() for parameters and semantics. This 334 implementation does nothing. 335 """ 336 pass 337 338 ## Rendering 339 340 def get_tile_coords(self): 341 return self._surface.get_tiles().keys() 342 343 def get_render_ops(self, spec): 344 """Get rendering instructions.""" 345 346 visible = self.visible 347 mode = self.mode 348 opacity = self.opacity 349 350 if spec.layers is not None: 351 if self not in spec.layers: 352 return [] 353 354 mode_default = lib.modes.default_mode() 355 if spec.previewing: 356 mode = mode_default 357 opacity = 1.0 358 visible = True 359 elif spec.solo: 360 if self is spec.current: 361 visible = True 362 363 if not visible: 364 return [] 365 366 ops = [] 367 if (spec.current_overlay is not None) and (self is spec.current): 368 # Temporary special effects, e.g. layer blink. 369 ops.append((rendering.Opcode.PUSH, None, None, None)) 370 ops.append(( 371 rendering.Opcode.COMPOSITE, self._surface, mode_default, 1.0, 372 )) 373 ops.extend(spec.current_overlay.get_render_ops(spec)) 374 ops.append(rendering.Opcode.POP, None, mode, opacity) 375 else: 376 # The 99%+ case☺ 377 ops.append(( 378 rendering.Opcode.COMPOSITE, self._surface, mode, opacity, 379 )) 380 return ops 381 382 ## Translating 383 384 def get_move(self, x, y): 385 """Get a translation/move object for this layer 386 387 :param x: Model X position of the start of the move 388 :param y: Model X position of the start of the move 389 :returns: A move object 390 391 """ 392 return SurfaceBackedLayerMove(self, x, y) 393 394 ## Saving 395 396 @lib.fileutils.via_tempfile 397 def save_as_png(self, filename, *rect, **kwargs): 398 """Save to a named PNG file 399 400 :param filename: filename to save to 401 :param *rect: rectangle to save, as a 4-tuple 402 :param **kwargs: passed to the surface's save_as_png() method 403 :rtype: Gdk.Pixbuf 404 """ 405 self._surface.save_as_png(filename, *rect, **kwargs) 406 407 def save_to_openraster(self, orazip, tmpdir, path, 408 canvas_bbox, frame_bbox, **kwargs): 409 """Saves the layer's data into an open OpenRaster ZipFile""" 410 rect = self.get_bbox() 411 return self._save_rect_to_ora(orazip, tmpdir, "layer", path, 412 frame_bbox, rect, **kwargs) 413 414 def queue_autosave(self, oradir, taskproc, manifest, bbox, **kwargs): 415 """Queues the layer for auto-saving""" 416 417 # Queue up a task which writes the surface as a PNG. This will 418 # be the file that's indexed by the <layer/>'s @src attribute. 419 # 420 # For looped layers - currently just the background layer - this 421 # PNG file has to fill the requested save bbox so that other 422 # apps will understand it. Other kinds of layer will just use 423 # their inherent data bbox size, which may be smaller. 424 # 425 # Background layers save a simple tile too, but with a 426 # mypaint-specific attribute name. If/when OpenRaster 427 # standardizes looped layer data, that code should be moved 428 # here. 429 430 png_basename = self.autosave_uuid + ".png" 431 png_relpath = os.path.join("data", png_basename) 432 png_path = os.path.join(oradir, png_relpath) 433 png_bbox = self._surface.looped and bbox or tuple(self.get_bbox()) 434 if self.autosave_dirty or not os.path.exists(png_path): 435 task = tiledsurface.PNGFileUpdateTask( 436 surface = self._surface, 437 filename = png_path, 438 rect = png_bbox, 439 alpha = (not self._surface.looped), # assume that means bg 440 **kwargs 441 ) 442 taskproc.add_work(task) 443 self.autosave_dirty = False 444 # Calculate appropriate offsets 445 png_x, png_y = png_bbox[0:2] 446 ref_x, ref_y = bbox[0:2] 447 x = png_x - ref_x 448 y = png_y - ref_y 449 assert (x == y == 0) or not self._surface.looped 450 # Declare and index what is about to be written 451 manifest.add(png_relpath) 452 elem = self._get_stackxml_element("layer", x, y) 453 elem.attrib["src"] = png_relpath 454 return elem 455 456 @staticmethod 457 def _make_refname(prefix, path, suffix, sep='-'): 458 """Internal: standardized filename for something with a path""" 459 assert "." in suffix 460 path_ref = sep.join([("%02d" % (n,)) for n in path]) 461 if not suffix.startswith("."): 462 suffix = sep + suffix 463 return "".join([prefix, sep, path_ref, suffix]) 464 465 def _save_rect_to_ora(self, orazip, tmpdir, prefix, path, 466 frame_bbox, rect, progress=None, **kwargs): 467 """Internal: saves a rectangle of the surface to an ORA zip""" 468 # Write PNG data via a tempfile 469 pngname = self._make_refname(prefix, path, ".png") 470 pngpath = os.path.join(tmpdir, pngname) 471 t0 = time.time() 472 self._surface.save_as_png(pngpath, *rect, progress=progress, **kwargs) 473 t1 = time.time() 474 logger.debug('%.3fs surface saving %r', t1 - t0, pngname) 475 # Archive and remove 476 storepath = "data/%s" % (pngname,) 477 orazip.write(pngpath, storepath) 478 os.remove(pngpath) 479 # Return details 480 png_bbox = tuple(rect) 481 png_x, png_y = png_bbox[0:2] 482 ref_x, ref_y = frame_bbox[0:2] 483 x = png_x - ref_x 484 y = png_y - ref_y 485 assert (x == y == 0) or not self._surface.looped 486 elem = self._get_stackxml_element("layer", x, y) 487 elem.attrib["src"] = storepath 488 return elem 489 490 ## Painting symmetry axis 491 492 def set_symmetry_state(self, active, center_x, center_y, 493 symmetry_type, rot_symmetry_lines): 494 """Set the surface's painting symmetry axis and active flag. 495 496 See `LayerBase.set_symmetry_state` for the params. 497 """ 498 self._surface.set_symmetry_state( 499 bool(active), 500 float(center_x), float(center_y), 501 int(symmetry_type), int(rot_symmetry_lines), 502 ) 503 504 ## Snapshots 505 506 def save_snapshot(self): 507 """Snapshots the state of the layer, for undo purposes""" 508 return SurfaceBackedLayerSnapshot(self) 509 510 ## Trimming 511 512 def get_trimmable(self): 513 return True 514 515 def trim(self, rect): 516 """Trim the layer to a rectangle, discarding data outside it 517 518 :param rect: A trimming rectangle in model coordinates 519 :type rect: tuple (x, y, w, h) 520 521 Only complete tiles are discarded by this method. 522 If a tile is neither fully inside nor fully outside the 523 rectangle, the part of the tile outside the rectangle will be 524 cleared. 525 """ 526 self.autosave_dirty = True 527 self._surface.trim(rect) 528 529 ## Cleanup 530 531 def remove_empty_tiles(self): 532 """Removes empty tiles. 533 534 :returns: Stats about the removal: (nremoved, ntotal) 535 :rtype: tuple 536 537 """ 538 removed, total = self._surface.remove_empty_tiles() 539 return (removed, total) 540 541 542class SurfaceBackedLayerMove (object): 543 """Move object wrapper for surface-backed layers 544 545 Layer Subclasses should extend this minimal implementation to 546 provide functionality for doing things other than the surface tiles 547 around. 548 549 """ 550 551 def __init__(self, layer, x, y): 552 super(SurfaceBackedLayerMove, self).__init__() 553 surface_move = layer._surface.get_move(x, y) 554 self._wrapped = surface_move 555 556 def update(self, dx, dy): 557 self._wrapped.update(dx, dy) 558 559 def cleanup(self): 560 self._wrapped.cleanup() 561 562 def process(self, n=200): 563 return self._wrapped.process(n) 564 565 566class SurfaceBackedLayerSnapshot (core.LayerBaseSnapshot): 567 """Minimal layer implementation's snapshot 568 569 Snapshots are stored in commands, and used to implement undo and redo. 570 They must be independent copies of the data, although copy-on-write 571 semantics are fine. Snapshot objects don't have to be _full and exact_ 572 clones of the layer's data, but they do need to capture _inherent_ 573 qualities of the layer. Mere metadata can be ignored. For the base 574 layer implementation, this means the surface tiles and the layer's 575 opacity. 576 """ 577 578 def __init__(self, layer): 579 super(SurfaceBackedLayerSnapshot, self).__init__(layer) 580 self.surface_sshot = layer._surface.save_snapshot() 581 582 def restore_to_layer(self, layer): 583 super(SurfaceBackedLayerSnapshot, self).restore_to_layer(layer) 584 layer._surface.load_snapshot(self.surface_sshot) 585 586 587class FileBackedLayer (SurfaceBackedLayer, core.ExternallyEditable): 588 """A layer with primarily file-based storage 589 590 File-based layers use temporary files for storage, and create one 591 file per edit of the layer in an external application. The only 592 operation which can change the file's content is editing the file in 593 an external app. The layer's position on the MyPaint canvas, its 594 mode and its opacity can be changed as normal. 595 596 The internal surface is used only to store and render a bitmap 597 preview of the layer's content. 598 599 """ 600 601 ## Class constants 602 603 ALLOWED_SUFFIXES = [] 604 REVISIONS_SUBDIR = u"revisions" 605 606 ## Construction 607 608 def __init__(self, x=0, y=0, **kwargs): 609 """Construct, with blank internal fields""" 610 super(FileBackedLayer, self).__init__(**kwargs) 611 self._workfile = None 612 self._x = int(round(x)) 613 self._y = int(round(y)) 614 self._keywords = kwargs.copy() 615 self._keywords["x"] = x 616 self._keywords["y"] = y 617 618 def _ensure_valid_working_file(self): 619 if self._workfile is not None: 620 return 621 ext = self.ALLOWED_SUFFIXES[0] 622 rev0_fp = tempfile.NamedTemporaryFile( 623 mode = "wb", 624 suffix = ext, 625 dir = self.revisions_dir, 626 delete = False, 627 ) 628 self.write_blank_backing_file(rev0_fp, **self._keywords) 629 rev0_fp.close() 630 self._workfile = _ManagedFile(rev0_fp.name) 631 logger.info("Loading new blank working file from %r", rev0_fp.name) 632 self.load_surface_from_pixbuf_file( 633 rev0_fp.name, 634 x=self._x, 635 y=self._y, 636 ) 637 redraw_bbox = self.get_full_redraw_bbox() 638 self._content_changed(*redraw_bbox) 639 640 @property 641 def revisions_dir(self): 642 cache_dir = self.root.doc.cache_dir 643 revisions_dir = os.path.join(cache_dir, self.REVISIONS_SUBDIR) 644 if not os.path.isdir(revisions_dir): 645 os.makedirs(revisions_dir) 646 return revisions_dir 647 648 def write_blank_backing_file(self, file, **kwargs): 649 """Write out the zeroth backing file revision. 650 651 :param file: open file-like object to write bytes into. 652 :param **kwargs: all construction params, including x and y. 653 654 This operation is deferred until the file is needed. 655 656 """ 657 raise NotImplementedError 658 659 def _load_surface_from_orazip_member(self, orazip, cache_dir, 660 src, progress, x, y): 661 """Loads the surface from a member of an OpenRaster zipfile 662 663 This override retains a managed copy of the extracted file in 664 the REVISIONS_SUBDIR of the cache folder. 665 666 """ 667 # Extract a copy of the file, and load that 668 tmpdir = os.path.join(cache_dir, "tmp") 669 if not os.path.isdir(tmpdir): 670 os.makedirs(tmpdir) 671 orazip.extract(src, path=tmpdir) 672 tmp_filename = os.path.join(tmpdir, src) 673 self.load_surface_from_pixbuf_file( 674 tmp_filename, 675 x, y, 676 progress, 677 ) 678 # Move it to the revisions subdir, and manage it there. 679 revisions_dir = os.path.join(cache_dir, self.REVISIONS_SUBDIR) 680 if not os.path.isdir(revisions_dir): 681 os.makedirs(revisions_dir) 682 self._workfile = _ManagedFile( 683 unicode(tmp_filename), 684 move=True, 685 dir=revisions_dir, 686 ) 687 # Record its loaded position 688 self._x = x 689 self._y = y 690 691 def _load_surface_from_oradir_member(self, oradir, cache_dir, 692 src, progress, x, y): 693 """Loads the surface from a file in an OpenRaster-like folder 694 695 This override makes a managed copy of the original file in the 696 REVISIONS_SUBDIR of the cache folder. 697 698 """ 699 # Load the displayed surface tiles 700 super(FileBackedLayer, self)._load_surface_from_oradir_member( 701 oradir, cache_dir, 702 src, progress, 703 x, y, 704 ) 705 # Copy it to the revisions subdir, and manage it there. 706 revisions_dir = os.path.join(cache_dir, self.REVISIONS_SUBDIR) 707 if not os.path.isdir(revisions_dir): 708 os.makedirs(revisions_dir) 709 self._workfile = _ManagedFile( 710 unicode(os.path.join(oradir, src)), 711 copy=True, 712 dir=revisions_dir, 713 ) 714 # Record its loaded position 715 self._x = x 716 self._y = y 717 718 ## Snapshots & cloning 719 720 def save_snapshot(self): 721 """Snapshots the state of the layer and its strokemap for undo""" 722 return FileBackedLayerSnapshot(self) 723 724 def __deepcopy__(self, memo): 725 clone = super(FileBackedLayer, self).__deepcopy__(memo) 726 clone._workfile = deepcopy(self._workfile) 727 return clone 728 729 ## Moving 730 731 def get_move(self, x, y): 732 """Start a new move for the layer""" 733 return FileBackedLayerMove(self, x, y) 734 735 ## Trimming (no-op for file-based layers) 736 737 def get_trimmable(self): 738 return False 739 740 def trim(self, rect): 741 pass 742 743 ## Saving 744 745 def save_to_openraster(self, orazip, tmpdir, path, 746 canvas_bbox, frame_bbox, **kwargs): 747 """Saves the working file to an OpenRaster zipfile""" 748 # No supercall in this override, but the base implementation's 749 # attributes method is useful. 750 ref_x, ref_y = frame_bbox[0:2] 751 x = self._x - ref_x 752 y = self._y - ref_y 753 elem = self._get_stackxml_element("layer", x, y) 754 # Pick a suitable name to store under. 755 self._ensure_valid_working_file() 756 src_path = unicode(self._workfile) 757 src_rootname, src_ext = os.path.splitext(src_path) 758 src_ext = src_ext.lower() 759 storename = self._make_refname("layer", path, src_ext) 760 storepath = "data/%s" % (storename,) 761 # Archive (but do not remove) the managed tempfile 762 orazip.write(src_path, storepath) 763 # Return details of what was written. 764 elem.attrib["src"] = unicode(storepath) 765 return elem 766 767 def queue_autosave(self, oradir, taskproc, manifest, bbox, **kwargs): 768 """Queues the layer for auto-saving""" 769 # Again, no supercall. Autosave the backing file by copying it. 770 ref_x, ref_y = bbox[0:2] 771 x = self._x - ref_x 772 y = self._y - ref_y 773 elem = self._get_stackxml_element("layer", x, y) 774 # Pick a suitable name to store under. 775 self._ensure_valid_working_file() 776 src_path = unicode(self._workfile) 777 src_rootname, src_ext = os.path.splitext(src_path) 778 src_ext = src_ext.lower() 779 final_basename = self.autosave_uuid + src_ext 780 final_relpath = os.path.join("data", final_basename) 781 final_path = os.path.join(oradir, final_relpath) 782 if self.autosave_dirty or not os.path.exists(final_path): 783 final_dir = os.path.join(oradir, "data") 784 tmp_fp = tempfile.NamedTemporaryFile( 785 mode = "wb", 786 prefix = final_basename, 787 dir = final_dir, 788 delete = False, 789 ) 790 tmp_path = tmp_fp.name 791 # Copy the managed tempfile now. 792 # Though perhaps this could be processed in chunks 793 # like other layers. 794 with open(src_path, "rb") as src_fp: 795 shutil.copyfileobj(src_fp, tmp_fp) 796 tmp_fp.close() 797 lib.fileutils.replace(tmp_path, final_path) 798 self.autosave_dirty = False 799 # Return details of what gets written. 800 manifest.add(final_relpath) 801 elem.attrib["src"] = unicode(final_relpath) 802 return elem 803 804 ## Editing via external apps 805 806 def new_external_edit_tempfile(self): 807 """Get a tempfile for editing in an external app""" 808 if self.root is None: 809 return 810 self._ensure_valid_working_file() 811 self._edit_tempfile = _ManagedFile( 812 unicode(self._workfile), 813 copy = True, 814 dir = self.external_edits_dir, 815 ) 816 return unicode(self._edit_tempfile) 817 818 def load_from_external_edit_tempfile(self, tempfile_path): 819 """Load content from an external-edit tempfile""" 820 redraw_bboxes = [] 821 redraw_bboxes.append(self.get_full_redraw_bbox()) 822 x = self._x 823 y = self._y 824 self.load_surface_from_pixbuf_file(tempfile_path, x=x, y=y) 825 redraw_bboxes.append(self.get_full_redraw_bbox()) 826 self._workfile = _ManagedFile( 827 tempfile_path, 828 copy = True, 829 dir = self.revisions_dir, 830 ) 831 self._content_changed(*tuple(core.combine_redraws(redraw_bboxes))) 832 self.autosave_dirty = True 833 834 835class FileBackedLayerSnapshot (SurfaceBackedLayerSnapshot): 836 """Snapshot subclass for file-backed layers""" 837 838 def __init__(self, layer): 839 super(FileBackedLayerSnapshot, self).__init__(layer) 840 self.workfile = layer._workfile 841 self.x = layer._x 842 self.y = layer._y 843 844 def restore_to_layer(self, layer): 845 super(FileBackedLayerSnapshot, self).restore_to_layer(layer) 846 layer._workfile = self.workfile 847 layer._x = self.x 848 layer._y = self.y 849 layer.autosave_dirty = True 850 851 852class FileBackedLayerMove (SurfaceBackedLayerMove): 853 """Move object wrapper for file-backed layers""" 854 855 def __init__(self, layer, x, y): 856 super(FileBackedLayerMove, self).__init__(layer, x, y) 857 self._layer = layer 858 self._start_x = layer._x 859 self._start_y = layer._y 860 861 def update(self, dx, dy): 862 super(FileBackedLayerMove, self).update(dx, dy) 863 # Update file position too. 864 self._layer._x = int(round(self._start_x + dx)) 865 self._layer._y = int(round(self._start_y + dy)) 866 # The file itself is the canonical source of the data, 867 # and just setting the position doesn't change that. 868 # So no need to set autosave_dirty here for these layers. 869 870 871## Utility classes 872 873 874class _ManagedFile (object): 875 """Working copy of a file, as used by file-backed layers 876 877 Managed files take control of an unmanaged file on disk when they 878 are created, and unlink it from the disk when their object is 879 destroyed. If you need a fresh copy to work on, the standard copy() 880 implementation handles that in the way you'd expect. 881 882 The underlying filename can be accessed by converting to `unicode`. 883 884 """ 885 886 def __init__(self, file_path, copy=False, move=False, dir=None): 887 """Initialize, taking control of an unmanaged file or a copy 888 889 :param unicode file_path: File to manage or manage a copy of 890 :param bool copy: Copy first, and manage the copy 891 :param bool move: Move first, and manage under the new name 892 :param unicode dir: Target folder for move or copy. 893 894 The file can be automatically copied or renamed first, 895 in which case the new file is managed instead of the original. 896 The new file will preserve the original's file extension, 897 but otherwise use UUID (random) syntax. 898 If `targdir` is undefined, this new file will be 899 created in the same folder as the original. 900 901 Creating these objects, or copying them, should only be 902 attempted from the main thread. 903 904 """ 905 assert isinstance(file_path, unicode) 906 assert os.path.isfile(file_path) 907 if dir: 908 assert os.path.isdir(dir) 909 super(_ManagedFile, self).__init__() 910 file_path = self._get_file_to_manage( 911 file_path, 912 copy=copy, 913 move=move, 914 dir=dir, 915 ) 916 file_dir, file_basename = os.path.split(file_path) 917 self._dir = file_dir 918 self._basename = file_basename 919 920 def __copy__(self): 921 """Shallow copies work just like deep copies""" 922 return deepcopy(self) 923 924 def __deepcopy__(self, memo): 925 """Deep-copying a _ManagedFile copies the file""" 926 orig_path = unicode(self) 927 clone_path = self._get_file_to_manage(orig_path, copy=True) 928 logger.debug("_ManagedFile: cloned %r as %r within %r", 929 self._basename, os.path.basename(clone_path), self._dir) 930 return _ManagedFile(clone_path) 931 932 @staticmethod 933 def _get_file_to_manage(orig_path, copy=False, move=False, dir=None): 934 """Obtain a file path to manage. Same params as constructor. 935 936 If asked to copy or rename first, 937 UUID-based naming is used without much error checking. 938 This should be sufficient for MyPaint's usage 939 because the document working dir is atomically constructed. 940 However it's not truly atomic or threadsafe. 941 942 """ 943 assert os.path.isfile(orig_path) 944 if not (copy or move): 945 return orig_path 946 orig_dir, orig_basename = os.path.split(orig_path) 947 orig_rootname, orig_ext = os.path.splitext(orig_basename) 948 if dir is None: 949 dir = orig_dir 950 new_unique_path = None 951 while new_unique_path is None: 952 new_rootname = unicode(uuid.uuid4()) 953 new_basename = new_rootname + orig_ext 954 new_path = os.path.join(dir, new_basename) 955 if os.path.exists(new_path): # yeah, paranoia 956 logger.warn("UUID clash: %r exists", new_path) 957 continue 958 if move: 959 os.rename(orig_path, new_path) 960 else: 961 shutil.copy2(orig_path, new_path) 962 new_unique_path = new_path 963 assert os.path.isfile(new_unique_path) 964 return new_unique_path 965 966 def __str__(self): 967 if PY3: 968 return self.__unicode__() 969 else: 970 return self.__bytes__() # Always an error under Py2 971 972 def __bytes__(self): 973 raise NotImplementedError("Use unicode strings for file names.") 974 975 def __unicode__(self): 976 file_path = os.path.join(self._dir, self._basename) 977 assert isinstance(file_path, unicode) 978 return file_path 979 980 def __repr__(self): 981 return "_ManagedFile(%r)" % (self,) 982 983 def __del__(self): 984 try: 985 file_path = unicode(self) 986 except Exception: 987 logger.exception("_ManagedFile: cleanup of incomplete object. " 988 "File may still exist on disk.") 989 return 990 if os.path.exists(file_path): 991 logger.debug("_ManagedFile: %r is no longer referenced, deleting", 992 file_path) 993 os.unlink(file_path) 994 else: 995 logger.debug("_ManagedFile: %r was already removed, not deleting", 996 file_path) 997 998 999## Data layer classes 1000 1001 1002class BackgroundLayer (SurfaceBackedLayer): 1003 """Background layer, with a repeating tiled image 1004 1005 By convention only, there is just a single non-editable background 1006 layer in any document, hidden behind an API in the document's 1007 RootLayerStack. In the MyPaint application, the working document's 1008 background layer cannot be manipulated by the user except through 1009 the background dialog. 1010 """ 1011 1012 # This could be generalized as a repeating tile for general use in 1013 # the layers stack, extending the FileBackedLayer concept. Think 1014 # textures! 1015 1016 # The legacy non-namespaced attribute is no longer _written_ 1017 # to files as of the 2.0 release. 2.0 .ora files will not 1018 # be stable in 1.2.1 and earlier in the general case, so 1019 # there is no point in pretending that they are. 1020 1021 # MyPaint will support _reading_ .ora files using the legacy 1022 # background tile attribute through the 2.x releases, but 1023 # no distinction is made when such files are subseqently saved. 1024 1025 ORA_BGTILE_LEGACY_ATTR = "background_tile" 1026 ORA_BGTILE_ATTR = "{%s}background-tile" % ( 1027 lib.xml.OPENRASTER_MYPAINT_NS, 1028 ) 1029 1030 def __init__(self, bg, **kwargs): 1031 if isinstance(bg, tiledsurface.Background): 1032 surface = bg 1033 else: 1034 surface = tiledsurface.Background(bg) 1035 super(BackgroundLayer, self).__init__(name=u"background", 1036 surface=surface, **kwargs) 1037 self.locked = False 1038 self.visible = True 1039 self.mode = lib.mypaintlib.CombineNormal 1040 self.opacity = 1.0 1041 1042 def set_surface(self, surface): 1043 """Sets the surface from a tiledsurface.Background""" 1044 assert isinstance(surface, tiledsurface.Background) 1045 self.autosave_dirty = True 1046 self._surface = surface 1047 1048 def save_to_openraster(self, orazip, tmpdir, path, 1049 canvas_bbox, frame_bbox, 1050 progress=None, **kwargs): 1051 1052 if not progress: 1053 progress = lib.feedback.Progress() 1054 progress.items = 2 1055 1056 # Item 1: save as a regular layer for other apps. 1057 # Background surfaces repeat, so just the bit filling the frame. 1058 elem = self._save_rect_to_ora( 1059 orazip, tmpdir, "background", path, 1060 frame_bbox, frame_bbox, 1061 progress=progress.open(), 1062 **kwargs 1063 ) 1064 1065 # Item 2: also save as single pattern (with corrected origin) 1066 x0, y0 = frame_bbox[0:2] 1067 x, y, w, h = self.get_bbox() 1068 1069 pngname = self._make_refname("background", path, "tile.png") 1070 tmppath = os.path.join(tmpdir, pngname) 1071 t0 = time.time() 1072 self._surface.save_as_png( 1073 tmppath, 1074 x=x + x0, 1075 y=y + y0, 1076 w=w, 1077 h=h, 1078 progress=progress.open(), 1079 **kwargs 1080 ) 1081 t1 = time.time() 1082 storename = 'data/%s' % (pngname,) 1083 logger.debug('%.3fs surface saving %s', t1 - t0, storename) 1084 orazip.write(tmppath, storename) 1085 os.remove(tmppath) 1086 elem.attrib[self.ORA_BGTILE_ATTR] = storename 1087 1088 progress.close() 1089 return elem 1090 1091 def queue_autosave(self, oradir, taskproc, manifest, bbox, **kwargs): 1092 """Queues the layer for auto-saving""" 1093 # Arrange for the tile PNG to be rewritten, if necessary 1094 tilepng_basename = self.autosave_uuid + "-tile.png" 1095 tilepng_relpath = os.path.join("data", tilepng_basename) 1096 manifest.add(tilepng_relpath) 1097 x0, y0 = bbox[0:2] 1098 x, y, w, h = self.get_bbox() 1099 tilepng_bbox = (x + x0, y + y0, w, h) 1100 tilepng_path = os.path.join(oradir, tilepng_relpath) 1101 if self.autosave_dirty or not os.path.exists(tilepng_path): 1102 task = tiledsurface.PNGFileUpdateTask( 1103 surface = self._surface, 1104 filename = tilepng_path, 1105 rect = tilepng_bbox, 1106 alpha = False, 1107 **kwargs 1108 ) 1109 taskproc.add_work(task) 1110 # Supercall will clear the dirty flag, no need to do it here 1111 elem = super(BackgroundLayer, self).queue_autosave( 1112 oradir, taskproc, manifest, bbox, 1113 **kwargs 1114 ) 1115 elem.attrib[self.ORA_BGTILE_LEGACY_ATTR] = tilepng_relpath 1116 elem.attrib[self.ORA_BGTILE_ATTR] = tilepng_relpath 1117 return elem 1118 1119 def save_snapshot(self): 1120 """Snapshots the state of the layer, for undo purposes""" 1121 return BackgroundLayerSnapshot(self) 1122 1123 1124class BackgroundLayerSnapshot (core.LayerBaseSnapshot): 1125 """Snapshot of a root layer stack's state""" 1126 1127 def __init__(self, layer): 1128 super(BackgroundLayerSnapshot, self).__init__(layer) 1129 self.surface = layer._surface 1130 1131 def restore_to_layer(self, layer): 1132 super(BackgroundLayerSnapshot, self).restore_to_layer(layer) 1133 layer._surface = self.surface 1134 1135 1136class VectorLayer (FileBackedLayer): 1137 """SVG-based vector layer 1138 1139 Vector layers respect a wider set of construction parameters than 1140 most layers: 1141 1142 :param float x: SVG document X coordinate, in model coords 1143 :param float y: SVG document Y coordinate, in model coords 1144 :param float w: SVG document width, in model pixels 1145 :param float h: SVG document height, in model pixels 1146 :param iterable outline: Initial shape, absolute ``(X, Y)`` points 1147 1148 The outline shape is drawn with a random color, and a thick dashed 1149 surround. It is intended to indicate where the SVG file goes on the 1150 canvas initially, to help avoid confusion. 1151 1152 The document bounding box should enclose all points of the outline. 1153 1154 """ 1155 1156 DEFAULT_NAME = C_( 1157 "layer default names", 1158 # TRANSLATORS: Short default name for vector (SVG/Inkscape) layers 1159 u"Vectors", 1160 ) 1161 1162 TYPE_DESCRIPTION = C_( 1163 "layer type descriptions", 1164 u"Vector Layer", 1165 ) 1166 1167 ALLOWED_SUFFIXES = [".svg"] 1168 1169 def get_icon_name(self): 1170 return "mypaint-layer-vector-symbolic" 1171 1172 def write_blank_backing_file(self, file, **kwargs): 1173 x = kwargs.get("x", 0) 1174 y = kwargs.get("y", 0) 1175 outline = kwargs.get("outline") 1176 if outline: 1177 outline = [(px - x, py - y) for (px, py) in outline] 1178 else: 1179 outline = [(0, 0), (0, N), (N, N), (N, 0)] 1180 svg = ( 1181 '<?xml version="1.0" encoding="UTF-8" standalone="no"?>' 1182 '<!-- Created by MyPaint (http://mypaint.org/) -->' 1183 '<svg version="1.1" width="{w}" height="{h}">' 1184 '<path d="M ' 1185 ).format(**kwargs) 1186 for px, py in outline: 1187 svg += "{x},{y} ".format(x=px, y=py) 1188 rgb = tuple([randint(0x33, 0x99) for i in range(3)]) 1189 col = "#%02x%02x%02x" % rgb 1190 svg += ( 1191 'Z" id="path0" ' 1192 'style="fill:none;stroke:{col};stroke-width:5;' 1193 'stroke-linecap:round;stroke-linejoin:round;' 1194 'stroke-dasharray:9, 9;stroke-dashoffset:0" />' 1195 '</svg>' 1196 ).format(col=col) 1197 1198 if not isinstance(svg, bytes): 1199 svg = svg.encode("utf-8") 1200 file.write(svg) 1201 1202 def flood_fill(self, fill_args, dst_layer=None): 1203 """Fill to dst_layer, with ref. to a rasterization of this layer. 1204 This implementation is virtually identical to the one in LayerStack. 1205 """ 1206 assert dst_layer is not self 1207 assert dst_layer is not None 1208 1209 root = self.root 1210 if root is None: 1211 raise ValueError( 1212 "Cannot flood_fill() into a vector layer which is not " 1213 "a descendent of a RootLayerStack." 1214 ) 1215 src = root.get_tile_accessible_layer_rendering(self) 1216 dst = dst_layer._surface 1217 return tiledsurface.flood_fill(src, fill_args, dst) 1218 1219 1220class FallbackBitmapLayer (FileBackedLayer): 1221 """An unpaintable, fallback bitmap layer""" 1222 1223 def get_icon_name(self): 1224 return "mypaint-layer-fallback-symbolic" 1225 1226 DEFAULT_NAME = C_( 1227 "layer default names", 1228 # TRANSLATORS: Short default name for renderable fallback layers 1229 "Bitmap", 1230 ) 1231 1232 TYPE_DESCRIPTION = C_( 1233 "layer type descriptions", 1234 u"Bitmap Data", 1235 ) 1236 1237 #: Any suffix is allowed, no preference for defaults 1238 ALLOWED_SUFFIXES = [""] 1239 1240 1241class FallbackDataLayer (FileBackedLayer): 1242 """An unpaintable, fallback, non-bitmap layer""" 1243 1244 def get_icon_name(self): 1245 return "mypaint-layer-fallback-symbolic" 1246 1247 DEFAULT_NAME = C_( 1248 "layer default names", 1249 # TRANSLATORS: Short default name for non-renderable fallback layers 1250 u"Data", 1251 ) 1252 1253 TYPE_DESCRIPTION = C_( 1254 "layer type descriptions", 1255 u"Unknown Data", 1256 ) 1257 1258 #: Any suffix is allowed, favour ".dat". 1259 ALLOWED_SUFFIXES = [".dat", ""] 1260 1261 #: Use a silly little icon so that the layer can be positioned 1262 FALLBACK_CONTENT = ( 1263 '''<?xml version="1.0" encoding="UTF-8" standalone="no"?> 1264 <svg width="64" height="64" version="1.1" 1265 xmlns="http://www.w3.org/2000/svg"> 1266 <rect width="62" height="62" x="1.5" y="1.5" 1267 style="{rectstyle};fill:{shadow};stroke:{shadow}" /> 1268 <rect width="62" height="62" x="0.5" y="0.5" 1269 style="{rectstyle};fill:{base};stroke:{basestroke}" /> 1270 <text x="33.5" y="50.5" 1271 style="{textstyle};fill:{textshadow};stroke:{textshadow}" 1272 >?</text> 1273 <text x="32.5" y="49.5" 1274 style="{textstyle};fill:{text};stroke:{textstroke}" 1275 >?</text> 1276 </svg>''').format( 1277 rectstyle="stroke-width:1", 1278 shadow="#000", 1279 base="#eee", 1280 basestroke="#fff", 1281 textstyle="text-align:center;text-anchor:middle;" 1282 "font-size:48px;font-weight:bold;font-family:sans", 1283 text="#9c0", 1284 textshadow="#360", 1285 textstroke="#ad1", 1286 ) 1287 1288 1289## User-paintable layer classes 1290 1291class SimplePaintingLayer (SurfaceBackedLayer): 1292 """A layer you can paint on, but not much else.""" 1293 1294 ## Class constants 1295 1296 ALLOWED_SUFFIXES = [".png"] 1297 1298 DEFAULT_NAME = C_( 1299 "layer default names", 1300 # TRANSLATORS: Default name for new normal, paintable layers 1301 u"Layer", 1302 ) 1303 1304 TYPE_DESCRIPTION = C_( 1305 "layer type descriptions", 1306 u"Painting Layer", 1307 ) 1308 1309 ## Flood fill 1310 1311 def get_fillable(self): 1312 """True if this layer currently accepts flood fill""" 1313 return not self.locked 1314 1315 def flood_fill(self, fill_args, dst_layer=None): 1316 """Fills a point on the surface with a color 1317 1318 :param fill_args: Parameters common to all fill calls 1319 :type fill_args: lib.floodfill.FloodFillArguments 1320 :param dst_layer: Optional target layer (default is self!) 1321 :type dst_layer: StrokemappedPaintingLayer 1322 1323 The `tolerance` parameter controls how much pixels are permitted to 1324 vary from the starting (target) color. This is calculated based on the 1325 rgba channel with the largest difference to the corresponding channel 1326 of the starting color, scaled to a number in [0,1] and also determines 1327 the alpha of filled pixels. 1328 1329 The default target layer is `self`. This method invalidates the filled 1330 area of the target layer's surface, queueing a redraw if it is part of 1331 a visible document. 1332 """ 1333 if dst_layer is None: 1334 dst_layer = self 1335 dst_layer.autosave_dirty = True # XXX hmm, not working? 1336 return self._surface.flood_fill(fill_args, dst=dst_layer._surface) 1337 1338 ## Simple painting 1339 1340 def get_paintable(self): 1341 """True if this layer currently accepts painting brushstrokes""" 1342 return ( 1343 self.visible 1344 and not self.locked 1345 and self.branch_visible 1346 and not self.branch_locked 1347 ) 1348 1349 1350 def stroke_to(self, brush, x, y, pressure, xtilt, ytilt, dtime, 1351 viewzoom, viewrotation, barrel_rotation): 1352 """Render a part of a stroke to the canvas surface 1353 1354 :param brush: The brush to use for rendering dabs 1355 :type brush: lib.brush.Brush 1356 :param x: Input event's X coord, translated to document coords 1357 :param y: Input event's Y coord, translated to document coords 1358 :param pressure: Input event's pressure 1359 :param xtilt: Input event's tilt component in the document X direction 1360 :param ytilt: Input event's tilt component in the document Y direction 1361 :param dtime: Time delta, in seconds 1362 :returns: whether the stroke should now be split 1363 :rtype: bool 1364 1365 This method renders zero or more dabs to the surface of this 1366 layer, but it won't affect any strokemap maintained by this 1367 object (even if subclasses add one). That's because this method 1368 is for tiny increments, not big brushstrokes. 1369 1370 Use this for the incremental painting of segments of a stroke 1371 corresponding to single input events. The return value tells 1372 the caller whether to finalize the lib.stroke.Stroke which is 1373 currently recording the user's input, and begin recording a new 1374 one. You can choose to ignore it if you're just using a 1375 SimplePaintingLayer and not recording strokes. 1376 1377 """ 1378 self._surface.begin_atomic() 1379 split = brush.stroke_to( 1380 self._surface.backend, x, y, 1381 pressure, xtilt, ytilt, dtime, viewzoom, 1382 viewrotation, barrel_rotation 1383 ) 1384 self._surface.end_atomic() 1385 self.autosave_dirty = True 1386 return split 1387 1388 @contextlib.contextmanager 1389 def cairo_request(self, x, y, w, h, mode=lib.modes.default_mode): 1390 """Get a Cairo context for a given area, then put back changes. 1391 1392 See lib.tiledsurface.MyPaintSurface.cairo_request() for details. 1393 This is just a wrapper. 1394 1395 """ 1396 with self._surface.cairo_request(x, y, w, h, mode) as cr: 1397 yield cr 1398 self.autosave_dirty = True 1399 1400 ## Type-specific stuff 1401 1402 def get_icon_name(self): 1403 return "mypaint-layer-painting-symbolic" 1404 1405 1406class StrokemappedPaintingLayer (SimplePaintingLayer): 1407 """Painting layer with a record of user brushstrokes. 1408 1409 This class definition adds a strokemap to the simple implementation. 1410 The stroke map is a stack of `strokemap.StrokeShape` objects in 1411 painting order, allowing strokes and their associated brush and 1412 color information to be picked from the canvas. 1413 1414 The caller of stroke_to() is expected to also maintain a current 1415 lib.stroke.Stroke object which records user input for the current 1416 stroke, but no shape info. When stroke_to() says to break the 1417 stroke, or when the caller wishes to break a stroke, feed these 1418 details back to the layer via add_stroke_shape() to update the 1419 strokemap. 1420 1421 """ 1422 1423 ## Class constants 1424 1425 # The un-namespaced legacy attribute name is deprecated since 1426 # MyPaint v1.2.0, and painting layers in OpenRaster files will not 1427 # be saved with it beginning with v2.0.0. 1428 # MyPaint will support reading .ora files using the legacy strokemap 1429 # attribute (and the "v2" strokemap format, if the format changes) 1430 # throughout v2.x. 1431 1432 _ORA_STROKEMAP_ATTR = "{%s}strokemap" % (lib.xml.OPENRASTER_MYPAINT_NS,) 1433 _ORA_STROKEMAP_LEGACY_ATTR = "mypaint_strokemap_v2" 1434 1435 ## Initializing & resetting 1436 1437 def __init__(self, **kwargs): 1438 super(StrokemappedPaintingLayer, self).__init__(**kwargs) 1439 #: Stroke map. 1440 #: List of strokemap.StrokeShape instances (not stroke.Stroke), 1441 #: ordered by depth. 1442 self.strokes = [] 1443 1444 def clear(self): 1445 """Clear both the surface and the strokemap""" 1446 super(StrokemappedPaintingLayer, self).clear() 1447 self.strokes = [] 1448 1449 def load_from_surface(self, surface): 1450 """Load the surface image's tiles from another surface""" 1451 super(StrokemappedPaintingLayer, self).load_from_surface(surface) 1452 self.strokes = [] 1453 1454 def load_from_openraster(self, orazip, elem, cache_dir, progress, 1455 x=0, y=0, invert_strokemaps=False, **kwargs): 1456 """Loads layer flags, PNG data, and strokemap from a .ora zipfile""" 1457 # Load layer tile data and flags 1458 super(StrokemappedPaintingLayer, self).load_from_openraster( 1459 orazip, 1460 elem, 1461 cache_dir, 1462 progress, 1463 x=x, y=y, 1464 **kwargs 1465 ) 1466 self._load_strokemap_from_ora( 1467 elem, x, y, invert_strokemaps, orazip=orazip 1468 ) 1469 1470 def load_from_openraster_dir(self, oradir, elem, cache_dir, progress, 1471 x=0, y=0, **kwargs): 1472 """Loads layer flags and data from an OpenRaster-style dir""" 1473 # Load layer tile data and flags 1474 super(StrokemappedPaintingLayer, self).load_from_openraster_dir( 1475 oradir, 1476 elem, 1477 cache_dir, 1478 progress, 1479 x=x, y=y, 1480 **kwargs 1481 ) 1482 self._load_strokemap_from_ora(elem, x, y, False, oradir=oradir) 1483 1484 def _load_strokemap_from_ora( 1485 self, elem, x, y, invert=False, orazip=None, oradir=None 1486 ): 1487 """Load the strokemap from a layer elem & an ora{zip|dir}.""" 1488 attrs = elem.attrib 1489 x += int(attrs.get('x', 0)) 1490 y += int(attrs.get('y', 0)) 1491 supported_strokemap_attrs = [ 1492 self._ORA_STROKEMAP_ATTR, 1493 self._ORA_STROKEMAP_LEGACY_ATTR, 1494 ] 1495 strokemap_name = None 1496 for attr_qname in supported_strokemap_attrs: 1497 strokemap_name = attrs.get(attr_qname, None) 1498 if strokemap_name is None: 1499 continue 1500 logger.debug( 1501 "Found strokemap %r in %r", 1502 strokemap_name, 1503 attr_qname, 1504 ) 1505 break 1506 if strokemap_name is None: 1507 return 1508 # This is a hacky way of identifying files which need their stroke 1509 # maps inverted, due to storing visually inconsistent colors. 1510 # These files are distinguished by lacking both the legacy strokemap 1511 # attribute and the eotf attribute. This support will be temporary. 1512 invert = invert and not attrs.get(self._ORA_STROKEMAP_LEGACY_ATTR) 1513 if orazip: 1514 if PY3: 1515 ioclass = BytesIO 1516 else: 1517 ioclass = StringIO 1518 sio = ioclass(orazip.read(strokemap_name)) 1519 self._load_strokemap_from_file(sio, x, y, invert) 1520 sio.close() 1521 elif oradir: 1522 with open(os.path.join(oradir, strokemap_name), "rb") as sfp: 1523 self._load_strokemap_from_file(sfp, x, y, invert) 1524 else: 1525 raise ValueError("either orazip or oradir must be specified") 1526 1527 ## Stroke recording and rendering 1528 1529 def render_stroke(self, stroke): 1530 """Render a whole captured stroke to the canvas 1531 1532 :param stroke: The stroke to render 1533 :type stroke: lib.stroke.Stroke 1534 """ 1535 stroke.render(self._surface) 1536 self.autosave_dirty = True 1537 1538 def add_stroke_shape(self, stroke, before): 1539 """Adds a rendered stroke's shape to the strokemap 1540 1541 :param stroke: the stroke sequence which has been rendered 1542 :type stroke: lib.stroke.Stroke 1543 :param before: layer snapshot taken before the stroke started 1544 :type before: lib.layer.StrokemappedPaintingLayerSnapshot 1545 1546 The StrokeMap is a stack of lib.strokemap.StrokeShape objects which 1547 encapsulate the shape of a rendered stroke, and the brush settings 1548 which were used to render it. The shape of the rendered stroke is 1549 determined by visually diffing snapshots taken before the stroke 1550 started and now. 1551 1552 """ 1553 after_sshot = self._surface.save_snapshot() 1554 shape = lib.strokemap.StrokeShape.new_from_snapshots( 1555 before.surface_sshot, 1556 after_sshot, 1557 ) 1558 if shape is not None: 1559 shape.brush_string = stroke.brush_settings 1560 self.strokes.append(shape) 1561 1562 ## Snapshots 1563 1564 def save_snapshot(self): 1565 """Snapshots the state of the layer and its strokemap for undo""" 1566 return StrokemappedPaintingLayerSnapshot(self) 1567 1568 ## Translating 1569 1570 def get_move(self, x, y): 1571 """Get an interactive move object for the surface and its strokemap""" 1572 return StrokemappedPaintingLayerMove(self, x, y) 1573 1574 ## Trimming 1575 1576 def trim(self, rect): 1577 """Trim the layer and its strokemap""" 1578 super(StrokemappedPaintingLayer, self).trim(rect) 1579 empty_strokes = [] 1580 for stroke in self.strokes: 1581 if not stroke.trim(rect): 1582 empty_strokes.append(stroke) 1583 for stroke in empty_strokes: 1584 logger.debug("Removing emptied stroke %r", stroke) 1585 self.strokes.remove(stroke) 1586 1587 ## Strokemap load and save 1588 1589 def _load_strokemap_from_file(self, f, translate_x, translate_y, invert): 1590 assert not self.strokes 1591 brushes = [] 1592 x = int(translate_x // N) * N 1593 y = int(translate_y // N) * N 1594 dx = translate_x % N 1595 dy = translate_y % N 1596 while True: 1597 t = f.read(1) 1598 if t == b"b": 1599 length, = struct.unpack('>I', f.read(4)) 1600 tmp = f.read(length) 1601 b_string = zlib.decompress(tmp) 1602 if invert: 1603 b_string = BrushInfo.brush_string_inverted_eotf(b_string) 1604 brushes.append(b_string) 1605 elif t == b"s": 1606 brush_id, length = struct.unpack('>II', f.read(2 * 4)) 1607 stroke = lib.strokemap.StrokeShape() 1608 tmp = f.read(length) 1609 stroke.init_from_string(tmp, x, y) 1610 stroke.brush_string = brushes[brush_id] 1611 # Translate non-aligned strokes 1612 if (dx, dy) != (0, 0): 1613 stroke.translate(dx, dy) 1614 self.strokes.append(stroke) 1615 elif t == b"}": 1616 break 1617 else: 1618 errmsg = "Invalid strokemap (initial char=%r)" % (t,) 1619 raise ValueError(errmsg) 1620 1621 ## Strokemap querying 1622 1623 def get_stroke_info_at(self, x, y): 1624 """Get the stroke at the given point""" 1625 x, y = int(x), int(y) 1626 for s in reversed(self.strokes): 1627 if s.touches_pixel(x, y): 1628 return s 1629 1630 def get_last_stroke_info(self): 1631 if not self.strokes: 1632 return None 1633 return self.strokes[-1] 1634 1635 ## Saving 1636 1637 def save_to_openraster(self, orazip, tmpdir, path, 1638 canvas_bbox, frame_bbox, **kwargs): 1639 """Save the strokemap too, in addition to the base implementation""" 1640 # Save the layer normally 1641 1642 elem = super(StrokemappedPaintingLayer, self).save_to_openraster( 1643 orazip, tmpdir, path, 1644 canvas_bbox, frame_bbox, **kwargs 1645 ) 1646 # Store stroke shape data too 1647 x, y, w, h = self.get_bbox() 1648 if PY3: 1649 sio = BytesIO() 1650 else: 1651 sio = StringIO() 1652 t0 = time.time() 1653 _write_strokemap(sio, self.strokes, -x, -y) 1654 t1 = time.time() 1655 data = sio.getvalue() 1656 sio.close() 1657 datname = self._make_refname("layer", path, "strokemap.dat") 1658 logger.debug("%.3fs strokemap saving %r", t1 - t0, datname) 1659 storepath = "data/%s" % (datname,) 1660 helpers.zipfile_writestr(orazip, storepath, data) 1661 # Add strokemap XML attrs and return. 1662 # See comment above for compatibility strategy. 1663 elem.attrib[self._ORA_STROKEMAP_ATTR] = storepath 1664 return elem 1665 1666 def queue_autosave(self, oradir, taskproc, manifest, bbox, **kwargs): 1667 """Queues the layer for auto-saving""" 1668 dat_basename = u"%s-strokemap.dat" % (self.autosave_uuid,) 1669 dat_relpath = os.path.join("data", dat_basename) 1670 dat_path = os.path.join(oradir, dat_relpath) 1671 # Have to do this before the supercall because that will clear 1672 # the dirty flag. 1673 if self.autosave_dirty or not os.path.exists(dat_path): 1674 x, y, w, h = self.get_bbox() 1675 task = _StrokemapFileUpdateTask( 1676 self.strokes, 1677 dat_path, 1678 -x, -y, 1679 ) 1680 taskproc.add_work(task) 1681 # Supercall to queue saving PNG and obtain basic XML 1682 elem = super(StrokemappedPaintingLayer, self).queue_autosave( 1683 oradir, taskproc, manifest, bbox, 1684 **kwargs 1685 ) 1686 # Add strokemap XML attrs and return. 1687 # See comment above for compatibility strategy. 1688 elem.attrib[self._ORA_STROKEMAP_ATTR] = dat_relpath 1689 manifest.add(dat_relpath) 1690 return elem 1691 1692 1693class PaintingLayer (StrokemappedPaintingLayer, core.ExternallyEditable): 1694 """The normal paintable bitmap layer that the user sees.""" 1695 1696 def __init__(self, **kwargs): 1697 super(PaintingLayer, self).__init__(**kwargs) 1698 self._external_edit = None 1699 1700 def new_external_edit_tempfile(self): 1701 """Get a tempfile for editing in an external app""" 1702 # Uniquely named tempfile. Will be overwritten. 1703 if not self.root: 1704 return 1705 tmp_filename = os.path.join( 1706 self.external_edits_dir, 1707 u"%s%s" % (unicode(uuid.uuid4()), u".png"), 1708 ) 1709 # Overwrite, saving only the data area. 1710 # Record the data area for later. 1711 rect = self.get_bbox() 1712 if rect.w <= 0: 1713 rect.w = N 1714 if rect.h <= 0: 1715 rect.h = N 1716 self._surface.save_as_png(tmp_filename, *rect, alpha=True) 1717 edit_info = (tmp_filename, _ManagedFile(tmp_filename), rect) 1718 self._external_edit = edit_info 1719 return tmp_filename 1720 1721 def load_from_external_edit_tempfile(self, tempfile_path): 1722 """Load content from an external-edit tempfile""" 1723 # Try to load the layer data back where it came from. 1724 # Only works if the file being loaded is the one most recently 1725 # created using new_external_edit_tempfile(). 1726 x, y, __, __ = self.get_bbox() 1727 edit_info = self._external_edit 1728 if edit_info: 1729 tmp_filename, __, rect = edit_info 1730 if tempfile_path == tmp_filename: 1731 x, y, __, __ = rect 1732 redraw_bboxes = [] 1733 redraw_bboxes.append(self.get_full_redraw_bbox()) 1734 self.load_surface_from_pixbuf_file(tempfile_path, x=x, y=y) 1735 redraw_bboxes.append(self.get_full_redraw_bbox()) 1736 self._content_changed(*tuple(core.combine_redraws(redraw_bboxes))) 1737 self.autosave_dirty = True 1738 1739 1740## Stroke-mapped layer implementation details and helpers 1741 1742def _write_strokemap(f, strokes, dx, dy): 1743 brush2id = {} 1744 for stroke in strokes: 1745 _write_strokemap_stroke(f, stroke, brush2id, dx, dy) 1746 f.write(b'}') 1747 1748 1749def _write_strokemap_stroke(f, stroke, brush2id, dx, dy): 1750 1751 # save brush (if not already recorderd) 1752 b = stroke.brush_string 1753 if b not in brush2id: 1754 brush2id[b] = len(brush2id) 1755 if isinstance(b, unicode): 1756 b = b.encode("utf-8") 1757 b = zlib.compress(b) 1758 f.write(b'b') 1759 f.write(struct.pack('>I', len(b))) 1760 f.write(b) 1761 1762 # save stroke 1763 s = stroke.save_to_string(dx, dy) 1764 f.write(b's') 1765 f.write(struct.pack('>II', brush2id[stroke.brush_string], len(s))) 1766 f.write(s) 1767 1768 1769class _StrokemapFileUpdateTask (object): 1770 """Updates a strokemap file in chunked calls (for autosave)""" 1771 1772 def __init__(self, strokes, filename, dx, dy): 1773 super(_StrokemapFileUpdateTask, self).__init__() 1774 tmp = tempfile.NamedTemporaryFile( 1775 mode = "wb", 1776 prefix = os.path.basename(filename), 1777 dir = os.path.dirname(filename), 1778 delete = False, 1779 ) 1780 self._tmp = tmp 1781 self._final_name = filename 1782 self._dx = dx 1783 self._dy = dy 1784 self._brush2id = {} 1785 self._strokes = strokes[:] 1786 self._strokes_i = 0 1787 logger.debug("autosave: scheduled update of %r", self._final_name) 1788 1789 def __call__(self): 1790 if self._tmp.closed: 1791 raise RuntimeError("Called too many times") 1792 if self._strokes_i < len(self._strokes): 1793 stroke = self._strokes[self._strokes_i] 1794 _write_strokemap_stroke( 1795 self._tmp, 1796 stroke, 1797 self._brush2id, 1798 self._dx, self._dy, 1799 ) 1800 self._strokes_i += 1 1801 return True 1802 else: 1803 self._tmp.write(b'}') 1804 self._tmp.close() 1805 lib.fileutils.replace(self._tmp.name, self._final_name) 1806 logger.debug("autosave: updated %r", self._final_name) 1807 return False 1808 1809 1810class StrokemappedPaintingLayerSnapshot (SurfaceBackedLayerSnapshot): 1811 """Snapshot subclass for painting layers with strokemaps""" 1812 1813 def __init__(self, layer): 1814 super(StrokemappedPaintingLayerSnapshot, self).__init__(layer) 1815 self.strokes = layer.strokes[:] 1816 1817 def restore_to_layer(self, layer): 1818 super(StrokemappedPaintingLayerSnapshot, self).restore_to_layer(layer) 1819 layer.strokes = self.strokes[:] 1820 layer.autosave_dirty = True 1821 1822 1823class StrokemappedPaintingLayerMove (SurfaceBackedLayerMove): 1824 """Move object wrapper for painting layers with strokemaps""" 1825 1826 def __init__(self, layer, x, y): 1827 super(StrokemappedPaintingLayerMove, self).__init__(layer, x, y) 1828 self._layer = layer 1829 self._final_dx = 0 1830 self._final_dy = 0 1831 1832 def update(self, dx, dy): 1833 super(StrokemappedPaintingLayerMove, self).update(dx, dy) 1834 self._final_dx = dx 1835 self._final_dy = dy 1836 1837 def cleanup(self): 1838 super(StrokemappedPaintingLayerMove, self).cleanup() 1839 dx = self._final_dx 1840 dy = self._final_dy 1841 # Arrange for the strokemap to be moved too; 1842 # this happens in its own background idler. 1843 for stroke in self._layer.strokes: 1844 stroke.translate(dx, dy) 1845 # Minor problem: huge strokemaps take a long time to move, and the 1846 # translate must be forced to completion before drawing or any 1847 # further layer moves. This can cause apparent hangs for no 1848 # reason later on. Perhaps it would be better to process them 1849 # fully in this hourglass-cursor phase after all? 1850 # The tile memory is the canonical source of a painting layer, 1851 # so we'll need to autosave it. 1852 self._layer.autosave_dirty = True 1853 1854 1855## Module testing 1856 1857 1858def _test(): 1859 """Run doctest strings""" 1860 import doctest 1861 doctest.testmod(optionflags=doctest.ELLIPSIS) 1862 1863 1864if __name__ == '__main__': 1865 logging.basicConfig(level=logging.DEBUG) 1866 _test() 1867