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