OSDN Git Service

tablet detection: workaround for laptop touchpad
[mypaint-anime/master.git] / lib / document.py
index a48dacf..0c3f561 100644 (file)
@@ -6,16 +6,22 @@
 # the Free Software Foundation; either version 2 of the License, or
 # (at your option) any later version.
 
-import os, zipfile, tempfile, time
+import os, zipfile, tempfile, time, traceback
 join = os.path.join
+from cStringIO import StringIO
 import xml.etree.ElementTree as ET
 from gtk import gdk
 import gobject, numpy
+from gettext import gettext as _
 
 import helpers, tiledsurface, pixbufsurface, backgroundsurface, mypaintlib
 import command, stroke, layer
 import brush
+
 N = tiledsurface.N
+LOAD_CHUNK_SIZE = 64*1024
+
+from layer import DEFAULT_COMPOSITE_OP, VALID_COMPOSITE_OPS
 
 class SaveLoadError(Exception):
     """Expected errors on loading or saving, like missing permissions or non-existing files."""
@@ -38,14 +44,72 @@ class Document():
     #   or discarded as empty before any other action is possible.
     #   (split_stroke)
 
-    def __init__(self):
-        self.brush = brush.Brush()
+    def __init__(self, brushinfo=None):
+        if not brushinfo:
+            brushinfo = brush.BrushInfo()
+            brushinfo.load_defaults()
+        self.brush = brush.Brush(brushinfo)
         self.stroke = None
         self.canvas_observers = []
         self.stroke_observers = [] # callback arguments: stroke, brush (brush is a temporary read-only convenience object)
         self.doc_observers = []
+        self.frame_observers = []
+        self.command_stack_observers = []
         self.clear(True)
 
+        self._frame = [0, 0, 0, 0]
+        self._frame_enabled = False
+        # Used by move_frame() to accumulate values
+        self._frame_dx = 0.0
+        self._frame_dy = 0.0
+
+    def get_frame(self):
+        return self._frame
+
+    def move_frame(self, dx=0.0, dy=0.0):
+        """Move the frame. Accumulates changes and moves the frame once
+        the accumulated change reaches the minimum move step."""
+        # FIXME: Should be 1 (pixel aligned), not tile aligned
+        # This is due to PNG saving having to be tile aligned
+        min_step = N
+
+        def round_to_n(value, n):
+            return int(round(value/n)*n)
+
+        x, y, w, h = self.get_frame()
+
+        self._frame_dx += dx
+        self._frame_dy += dy
+        step_x = round_to_n(self._frame_dx, min_step)
+        step_y = round_to_n(self._frame_dy, min_step)
+
+        if step_x:
+            self.set_frame(x=x+step_x)
+            self._frame_dx -= step_x
+
+        if step_y:
+            self.set_frame(y=y+step_y)
+            self._frame_dy -= step_y
+
+    def set_frame(self, x=None, y=None, width=None, height=None):
+        """Set the size of the frame. Pass None to indicate no-change."""
+
+        for i, var in enumerate([x, y, width, height]):
+            if not var is None:
+                # FIXME: must be aligned to tile size due to PNG saving
+                assert not var % N, "Frame size must be aligned to tile size"
+                self._frame[i] = var
+
+        for f in self.frame_observers: f()
+
+    def get_frame_enabled(self):
+        return self._frame_enabled
+
+    def set_frame_enabled(self, enabled):
+        self._frame_enabled = enabled
+        for f in self.frame_observers: f()
+    frame_enabled = property(get_frame_enabled)
+
     def call_doc_observers(self):
         for f in self.doc_observers:
             f(self)
@@ -56,7 +120,9 @@ class Document():
         if not init:
             bbox = self.get_bbox()
         # throw everything away, including undo stack
+
         self.command_stack = command.CommandStack()
+        self.command_stack.stack_observers = self.command_stack_observers
         self.set_background((255, 255, 255))
         self.layers = []
         self.layer_idx = None
@@ -89,33 +155,46 @@ class Document():
     def select_layer(self, idx):
         self.do(command.SelectLayer(self, idx))
 
-    def move_layer(self, was_idx, new_idx):
-        self.do(command.MoveLayer(self, was_idx, new_idx))
+    def move_layer(self, was_idx, new_idx, select_new=False):
+        self.do(command.MoveLayer(self, was_idx, new_idx, select_new))
+
+    def duplicate_layer(self, insert_idx=None, name=''):
+        self.do(command.DuplicateLayer(self, insert_idx, name))
+
+    def reorder_layers(self, new_layers):
+        self.do(command.ReorderLayers(self, new_layers))
 
     def clear_layer(self):
-        self.do(command.ClearLayer(self))
+        if not self.layer.is_empty():
+            self.do(command.ClearLayer(self))
 
-    def stroke_to(self, dtime, x, y, pressure):
+    def stroke_to(self, dtime, x, y, pressure, xtilt, ytilt):
         if not self.stroke:
             self.stroke = stroke.Stroke()
             self.stroke.start_recording(self.brush)
             self.snapshot_before_stroke = self.layer.save_snapshot()
-        self.stroke.record_event(dtime, x, y, pressure)
+        self.stroke.record_event(dtime, x, y, pressure, xtilt, ytilt)
 
-        l = self.layer
-        l.surface.begin_atomic()
-        split = self.brush.stroke_to (l.surface, x, y, pressure, dtime)
-        l.surface.end_atomic()
+        split = self.layer.stroke_to(self.brush, x, y,
+                                pressure, xtilt, ytilt, dtime)
 
         if split:
             self.split_stroke()
 
+    def redo_last_stroke_with_different_brush(self, brush):
+        cmd = self.get_last_command()
+        if not isinstance(cmd, command.Stroke):
+            return
+        cmd = self.undo()
+        assert isinstance(cmd, command.Stroke)
+        new_stroke = cmd.stroke.copy_using_different_brush(brush)
+        snapshot_before = self.layer.save_snapshot()
+        new_stroke.render(self.layer._surface)
+        self.do(command.Stroke(self, new_stroke, snapshot_before))
+
     def straight_line(self, src, dst):
         self.split_stroke()
-        # TODO: undo last stroke if it was very short... (but not at document level?)
-        real_brush = self.brush
-        self.brush = brush.Brush()
-        self.brush.copy_settings_from(real_brush)
+        self.brush.reset() # reset dynamic states (eg. filtered velocity)
 
         duration = 3.0
         pressure = 0.3
@@ -125,11 +204,11 @@ class Document():
         # rest the brush in src for a minute, to avoid interpolation
         # from the upper left corner (states are zero) (FIXME: the
         # brush should handle this on its own, maybe?)
-        self.stroke_to(60.0, x[0], y[0], 0.0)
+        self.stroke_to(60.0, x[0], y[0], 0.0, 0.0, 0.0)
         for i in xrange(N):
-            self.stroke_to(duration/N, x[i], y[i], pressure)
+            self.stroke_to(duration/N, x[i], y[i], pressure, 0.0, 0.0)
         self.split_stroke()
-        self.brush = real_brush
+        self.brush.reset()
 
 
     def layer_modified_cb(self, *args):
@@ -163,20 +242,22 @@ class Document():
         self.split_stroke()
         return self.command_stack.get_last_command()
 
-    def set_brush(self, brush):
-        self.split_stroke()
-        self.brush.copy_settings_from(brush)
-
     def get_bbox(self):
         res = helpers.Rect()
         for layer in self.layers:
             # OPTIMIZE: only visible layers...
             # careful: currently saving assumes that all layers are included
-            bbox = layer.surface.get_bbox()
+            bbox = layer.get_bbox()
             res.expandToIncludeRect(bbox)
         return res
 
-    def blit_tile_into(self, dst_8bit, tx, ty, mipmap=0, layers=None, background=None):
+    def get_effective_bbox(self):
+        """Return the effective bounding box of the document.
+        If the frame is enabled, this is the bounding box of the frame, 
+        else the (dynamic) bounding box of the document."""
+        return self.get_frame() if self.frame_enabled else self.get_bbox()
+
+    def blit_tile_into(self, dst_8bit, tx, ty, mipmap_level=0, layers=None, background=None):
         if layers is None:
             layers = self.layers
         if background is None:
@@ -185,32 +266,64 @@ class Document():
         assert dst_8bit.dtype == 'uint8'
         dst = numpy.empty((N, N, 3), dtype='uint16')
 
-        background.blit_tile_into(dst, tx, ty, mipmap)
+        background.blit_tile_into(dst, tx, ty, mipmap_level)
 
         for layer in layers:
-            surface = layer.surface
-            surface.composite_tile_over(dst, tx, ty, mipmap_level=mipmap, opacity=layer.opacity)
+            surface = layer._surface
+            surface.composite_tile(dst, tx, ty,
+                    mipmap_level=mipmap_level,
+                    opacity=layer.effective_opacity,
+                    mode=layer.compositeop)
 
         mypaintlib.tile_convert_rgb16_to_rgb8(dst, dst_8bit)
 
-    def add_layer(self, insert_idx=None, after=None):
-        self.do(command.AddLayer(self, insert_idx, after))
+    def add_layer(self, insert_idx=None, after=None, name=''):
+        self.do(command.AddLayer(self, insert_idx, after, name))
 
     def remove_layer(self,layer=None):
-        self.do(command.RemoveLayer(self,layer))
+        if len(self.layers) > 1:
+            self.do(command.RemoveLayer(self,layer))
+        else:
+            self.clear_layer()
 
-    def merge_layer(self, dst_idx):
+    def merge_layer_down(self):
+        dst_idx = self.layer_idx - 1
+        if dst_idx < 0:
+            return False
         self.do(command.MergeLayer(self, dst_idx))
+        return True
 
     def load_layer_from_pixbuf(self, pixbuf, x=0, y=0):
         arr = helpers.gdkpixbuf2numpy(pixbuf)
         self.do(command.LoadLayer(self, arr, x, y))
 
-    def set_layer_opacity(self, opacity):
+    def set_layer_visibility(self, visible, layer):
+        cmd = self.get_last_command()
+        if isinstance(cmd, command.SetLayerVisibility) and cmd.layer is layer:
+            self.undo()
+        self.do(command.SetLayerVisibility(self, visible, layer))
+
+    def set_layer_locked(self, locked, layer):
+        cmd = self.get_last_command()
+        if isinstance(cmd, command.SetLayerLocked) and cmd.layer is layer:
+            self.undo()
+        self.do(command.SetLayerLocked(self, locked, layer))
+
+    def set_layer_opacity(self, opacity, layer=None):
+        """Sets the opacity of a layer. If layer=None, works on the current layer"""
         cmd = self.get_last_command()
         if isinstance(cmd, command.SetLayerOpacity):
             self.undo()
-        self.do(command.SetLayerOpacity(self, opacity))
+        self.do(command.SetLayerOpacity(self, opacity, layer))
+
+    def set_layer_compositeop(self, compositeop, layer=None):
+        """Sets the composition-operation of a layer. If layer=None, works on the current layer"""
+        if compositeop not in VALID_COMPOSITE_OPS:
+            compositeop = DEFAULT_COMPOSITE_OP
+        cmd = self.get_last_command()
+        if isinstance(cmd, command.SetLayerCompositeOp):
+            self.undo()
+        self.do(command.SetLayerCompositeOp(self, compositeop, layer))
 
     def set_background(self, obj):
         # This is not an undoable action. One reason is that dragging
@@ -223,89 +336,140 @@ class Document():
         self.invalidate_all()
 
     def load_from_pixbuf(self, pixbuf):
+        """Load a document from a pixbuf."""
         self.clear()
         self.load_layer_from_pixbuf(pixbuf)
+        self.set_frame(*self.get_bbox())
 
     def is_layered(self):
         count = 0
         for l in self.layers:
-            if not l.surface.is_empty():
+            if not l.is_empty():
                 count += 1
         return count > 1
 
+    def is_empty(self):
+        return len(self.layers) == 1 and self.layer.is_empty()
+
     def save(self, filename, **kwargs):
         self.split_stroke()
-        trash, ext = os.path.splitext(filename)
+        junk, ext = os.path.splitext(filename)
         ext = ext.lower().replace('.', '')
         save = getattr(self, 'save_' + ext, self.unsupported)
-        try:        
+        try:
             save(filename, **kwargs)
         except gobject.GError, e:
-            if  e.code == 5:
+            traceback.print_exc()
+            if e.code == 5:
                 #add a hint due to a very consfusing error message when there is no space left on device
-                raise SaveLoadError, 'Unable to save: ' + e.message +  '\nDo you have enough space left on the device?'
+                raise SaveLoadError, _('Unable to save: %s\nDo you have enough space left on the device?') % e.message
             else:
-                raise SaveLoadError, 'Unable to save: ' + e.message
+                raise SaveLoadError, _('Unable to save: %s') % e.message
         except IOError, e:
-            raise SaveLoadError, 'Unable to save: ' + e.strerror
+            traceback.print_exc()
+            raise SaveLoadError, _('Unable to save: %s') % e.strerror
         self.unsaved_painting_time = 0.0
 
-    def load(self, filename):
+    def load(self, filename, **kwargs):
         if not os.path.isfile(filename):
-            raise SaveLoadError, 'File does not exist: ' + repr(filename)
+            raise SaveLoadError, _('File does not exist: %s') % repr(filename)
         if not os.access(filename,os.R_OK):
-            raise SaveLoadError, 'You do not have the necessary permissions to open file: ' + repr(filename)
-        trash, ext = os.path.splitext(filename)
+            raise SaveLoadError, _('You do not have the necessary permissions to open file: %s') % repr(filename)
+        junk, ext = os.path.splitext(filename)
         ext = ext.lower().replace('.', '')
         load = getattr(self, 'load_' + ext, self.unsupported)
-        load(filename)
+        try:
+            load(filename, **kwargs)
+        except gobject.GError, e:
+            traceback.print_exc()
+            raise SaveLoadError, _('Error while loading: GError %s') % e
+        except IOError, e:
+            traceback.print_exc()
+            raise SaveLoadError, _('Error while loading: IOError %s') % e
         self.command_stack.clear()
         self.unsaved_painting_time = 0.0
         self.call_doc_observers()
 
-    def unsupported(self, filename):
-        raise SaveLoadError, 'Unknown file format extension: ' + repr(filename)
+    def unsupported(self, filename, *args, **kwargs):
+        raise SaveLoadError, _('Unknown file format extension: %s') % repr(filename)
 
-    def render_as_pixbuf(self, *args):
-        return pixbufsurface.render_as_pixbuf(self, *args)
+    def render_as_pixbuf(self, *args, **kwargs):
+        return pixbufsurface.render_as_pixbuf(self, *args, **kwargs)
 
-    def save_png(self, filename, compression=2, alpha=False, multifile=False):
+    def render_thumbnail(self):
+        t0 = time.time()
+        x, y, w, h = self.get_effective_bbox()
+        if w == 0 or h == 0:
+            # workaround to save empty documents
+            x, y, w, h = 0, 0, tiledsurface.N, tiledsurface.N
+        mipmap_level = 0
+        while mipmap_level < tiledsurface.MAX_MIPMAP_LEVEL and max(w, h) >= 512:
+            mipmap_level += 1
+            x, y, w, h = x/2, y/2, w/2, h/2
+
+        pixbuf = self.render_as_pixbuf(x, y, w, h, mipmap_level=mipmap_level)
+        assert pixbuf.get_width() == w and pixbuf.get_height() == h
+        pixbuf = helpers.scale_proportionally(pixbuf, 256, 256)
+        print 'Rendered thumbnail in', time.time() - t0, 'seconds.'
+        return pixbuf
+
+    def save_png(self, filename, alpha=False, multifile=False, **kwargs):
+        doc_bbox = self.get_effective_bbox()
         if multifile:
-            self.save_multifile_png(filename, compression)
+            self.save_multifile_png(filename, **kwargs)
         else:
             if alpha:
                 tmp_layer = layer.Layer()
                 for l in self.layers:
                     l.merge_into(tmp_layer)
-                pixbuf = tmp_layer.surface.render_as_pixbuf()
+                tmp_layer.save_as_png(filename, *doc_bbox)
             else:
-                pixbuf = self.render_as_pixbuf()
-            pixbuf.save(filename, 'png', {'compression':str(compression)})
+                pixbufsurface.save_as_png(self, filename, *doc_bbox, alpha=False, **kwargs)
 
-    def save_multifile_png(self, filename, compression=2, alpha=False):
+    def save_multifile_png(self, filename, alpha=False, **kwargs):
         prefix, ext = os.path.splitext(filename)
         # if we have a number already, strip it
         l = prefix.rsplit('.', 1)
         if l[-1].isdigit():
             prefix = l[0]
-        doc_bbox = self.get_bbox()
+        doc_bbox = self.get_effective_bbox()
         for i, l in enumerate(self.layers):
             filename = '%s.%03d%s' % (prefix, i+1, ext)
-            l.surface.save(filename, *doc_bbox)
-
-    def load_png(self, filename):
-        self.load_from_pixbuf(gdk.pixbuf_new_from_file(filename))
-
-    def load_jpg(self, filename):
-        self.load_from_pixbuf(gdk.pixbuf_new_from_file(filename))
-    load_jpeg = load_jpg
-
-    def save_jpg(self, filename, quality=90):
-        pixbuf = self.render_as_pixbuf()
+            l.save_as_png(filename, *doc_bbox, **kwargs)
+
+    @staticmethod
+    def _pixbuf_from_stream(fp, feedback_cb=None):
+        loader = gdk.PixbufLoader()
+        while True:
+            if feedback_cb is not None:
+                feedback_cb()
+            buf = fp.read(LOAD_CHUNK_SIZE)
+            if buf == '':
+                break
+            loader.write(buf)
+        loader.close()
+        return loader.get_pixbuf()
+
+    def load_from_pixbuf_file(self, filename, feedback_cb=None):
+        fp = open(filename, 'rb')
+        pixbuf = self._pixbuf_from_stream(fp, feedback_cb)
+        fp.close()
+        self.load_from_pixbuf(pixbuf)
+
+    load_png = load_from_pixbuf_file
+    load_jpg = load_from_pixbuf_file
+    load_jpeg = load_from_pixbuf_file
+
+    def save_jpg(self, filename, quality=90, **kwargs):
+        x, y, w, h = self.get_effective_bbox()
+        if w == 0 or h == 0:
+            x, y, w, h = 0, 0, N, N # allow to save empty documents
+        pixbuf = self.render_as_pixbuf(x, y, w, h, **kwargs)
         pixbuf.save(filename, 'jpeg', options={'quality':str(quality)})
+
     save_jpeg = save_jpg
 
-    def save_ora(self, filename, options=None):
+    def save_ora(self, filename, options=None, **kwargs):
         print 'save_ora:'
         t0 = time.time()
         tempdir = tempfile.mkdtemp('mypaint')
@@ -319,7 +483,7 @@ class Document():
         write_file_str('mimetype', 'image/openraster') # must be the first file
         image = ET.Element('image')
         stack = ET.SubElement(image, 'stack')
-        x0, y0, w0, h0 = self.get_bbox()
+        x0, y0, w0, h0 = self.get_effective_bbox()
         a = image.attrib
         a['w'] = str(w0)
         a['h'] = str(h0)
@@ -327,15 +491,23 @@ class Document():
         def store_pixbuf(pixbuf, name):
             tmp = join(tempdir, 'tmp.png')
             t1 = time.time()
-            pixbuf.save(tmp, 'png', {'compression':'2'})
-            print '  %.3fs saving %s compression 2' % (time.time() - t1, name)
+            pixbuf.save(tmp, 'png')
+            print '  %.3fs pixbuf saving %s' % (time.time() - t1, name)
             z.write(tmp, name)
             os.remove(tmp)
 
-        def add_layer(x, y, opac, pixbuf, name, layer_name):
+        def store_surface(surface, name, rect=[]):
+            tmp = join(tempdir, 'tmp.png')
+            t1 = time.time()
+            surface.save_as_png(tmp, *rect, **kwargs)
+            print '  %.3fs surface saving %s' % (time.time() - t1, name)
+            z.write(tmp, name)
+            os.remove(tmp)
+
+        def add_layer(x, y, opac, surface, name, layer_name, visible=True, compositeop=DEFAULT_COMPOSITE_OP, rect=[]):
             layer = ET.Element('layer')
             stack.append(layer)
-            store_pixbuf(pixbuf, name)
+            store_surface(surface, name, rect)
             a = layer.attrib
             if layer_name:
                 a['name'] = layer_name
@@ -343,46 +515,46 @@ class Document():
             a['x'] = str(x)
             a['y'] = str(y)
             a['opacity'] = str(opac)
+            if compositeop not in VALID_COMPOSITE_OPS:
+                compositeop = DEFAULT_COMPOSITE_OP
+            a['composite-op'] = compositeop
+            if visible:
+                a['visibility'] = 'visible'
+            else:
+                a['visibility'] = 'hidden'
             return layer
 
         for idx, l in enumerate(reversed(self.layers)):
-            if l.surface.is_empty():
+            if l.is_empty():
                 continue
             opac = l.opacity
-            x, y, w, h = l.surface.get_bbox()
-            pixbuf = l.surface.render_as_pixbuf()
-
-            el = add_layer(x-x0, y-y0, opac, pixbuf, 'data/layer%03d.png' % idx, l.name)
+            x, y, w, h = l.get_bbox()
+            el = add_layer(x-x0, y-y0, opac, l._surface, 'data/layer%03d.png' % idx, l.name, l.visible, l.compositeop, rect=(x, y, w, h))
             # strokemap
-            data = l.save_strokemap_to_string(-x, -y)
+            sio = StringIO()
+            l.save_strokemap_to_file(sio, -x, -y)
+            data = sio.getvalue(); sio.close()
             name = 'data/layer%03d_strokemap.dat' % idx
-            el.attrib['mypaint_strokemap'] = name
+            el.attrib['mypaint_strokemap_v2'] = name
             write_file_str(name, data)
 
-
         # save background as layer (solid color or tiled)
-        s = pixbufsurface.Surface(x0, y0, w0, h0)
-        s.fill(self.background)
-        l = add_layer(0, 0, 1.0, s.pixbuf, 'data/background.png', 'background')
         bg = self.background
+        # save as fully rendered layer
+        x, y, w, h = self.get_bbox()
+        l = add_layer(x-x0, y-y0, 1.0, bg, 'data/background.png', 'background',
+                      DEFAULT_COMPOSITE_OP, rect=(x,y,w,h))
         x, y, w, h = bg.get_pattern_bbox()
-        pixbuf = pixbufsurface.render_as_pixbuf(bg, x+x0, y+y0, w, h, alpha=False)
-        store_pixbuf(pixbuf, 'data/background_tile.png')
+        # save as single pattern (with corrected origin)
+        store_surface(bg, 'data/background_tile.png', rect=(x+x0, y+y0, w, h))
         l.attrib['background_tile'] = 'data/background_tile.png'
 
-        # preview
+        # preview (256x256)
         t2 = time.time()
-        print '  starting to render image for thumbnail...'
-        pixbuf = self.render_as_pixbuf()
-        w, h = pixbuf.get_width(), pixbuf.get_height()
-        if w > h:
-            w, h = 256, max(h*256/w, 1)
-        else:
-            w, h = max(w*256/h, 1), 256
-        t1 = time.time()
-        pixbuf = pixbuf.scale_simple(w, h, gdk.INTERP_BILINEAR)
-        print '  %.3fs scaling thumbnail' % (time.time() - t1)
-        store_pixbuf(pixbuf, 'Thumbnails/thumbnail.png')
+        print '  starting to render full image for thumbnail...'
+
+        thumbnail_pixbuf = self.render_thumbnail()
+        store_pixbuf(thumbnail_pixbuf, 'Thumbnails/thumbnail.png')
         print '  total %.3fs spent on thumbnail' % (time.time() - t2)
 
         helpers.indent_etree(image)
@@ -397,24 +569,41 @@ class Document():
 
         print '%.3fs save_ora total' % (time.time() - t0)
 
-    def load_ora(self, filename):
+        return thumbnail_pixbuf
+
+    def load_ora(self, filename, feedback_cb=None):
+        """Loads from an OpenRaster file"""
         print 'load_ora:'
         t0 = time.time()
-        tempdir = tempfile.mkdtemp('mypaint')
         z = zipfile.ZipFile(filename)
         print 'mimetype:', z.read('mimetype').strip()
         xml = z.read('stack.xml')
         image = ET.fromstring(xml)
         stack = image.find('stack')
 
+        w = int(image.attrib['w'])
+        h = int(image.attrib['h'])
+
+        def round_up_to_n(value, n):
+            assert value >= 0, "function undefined for negative numbers"
+
+            residual = value % n
+            if residual:
+                value = value - residual + n
+            return int(value)
+
         def get_pixbuf(filename):
             t1 = time.time()
-            tmp = join(tempdir, 'tmp.png')
-            f = open(tmp, 'wb')
-            f.write(z.read(filename))
-            f.close()
-            res = gdk.pixbuf_new_from_file(tmp)
-            os.remove(tmp)
+
+            try:
+                fp = z.open(filename, mode='r')
+            except KeyError:
+                # support for bad zip files (saved by old versions of the GIMP ORA plugin)
+                fp = z.open(filename.encode('utf-8'), mode='r')
+                print 'WARNING: bad OpenRaster ZIP file. There is an utf-8 encoded filename that does not have the utf-8 flag set:', repr(filename)
+
+            res = self._pixbuf_from_stream(fp, feedback_cb)
+            fp.close()
             print '  %.3fs loading %s' % (time.time() - t1, filename)
             return res
 
@@ -437,6 +626,9 @@ class Document():
 
         self.clear() # this leaves one empty layer
         no_background = True
+        # FIXME: don't require tile alignment for frame
+        self.set_frame(width=round_up_to_n(w, N), height=round_up_to_n(h, N))
+
         for layer in get_layers_list(stack):
             a = layer.attrib
 
@@ -456,38 +648,44 @@ class Document():
                 continue
             pixbuf = get_pixbuf(src)
             name = a.get('name', '')
-
             x = int(a.get('x', '0'))
             y = int(a.get('y', '0'))
             opac = float(a.get('opacity', '1.0'))
-            self.add_layer(insert_idx=0)
+            compositeop = str(a.get('composite-op', DEFAULT_COMPOSITE_OP))
+            if compositeop not in VALID_COMPOSITE_OPS:
+                compositeop = DEFAULT_COMPOSITE_OP
+
+            visible = not 'hidden' in a.get('visibility', 'visible')
+            self.add_layer(insert_idx=0, name=name)
             last_pixbuf = pixbuf
             t1 = time.time()
             self.load_layer_from_pixbuf(pixbuf, x, y)
             layer = self.layers[0]
-            layer.name = name
-            layer.opacity = helpers.clamp(opac, 0.0, 1.0)
+
+            self.set_layer_opacity(helpers.clamp(opac, 0.0, 1.0), layer)
+            self.set_layer_compositeop(compositeop, layer)
+            self.set_layer_visibility(visible, layer)
             print '  %.3fs converting pixbuf to layer format' % (time.time() - t1)
             # strokemap
-            fname = a.get('mypaint_strokemap', None)
+            fname = a.get('mypaint_strokemap_v2', None)
             if fname:
                 if x % N or y % N:
                     print 'Warning: dropping non-aligned strokemap'
                 else:
-                    data = z.read(fname)
-                    self.layers[0].load_strokemap_from_string(data, x, y)
-
-        os.rmdir(tempdir)
+                    sio = StringIO(z.read(fname))
+                    layer.load_strokemap_from_file(sio, x, y)
+                    sio.close()
 
         if len(self.layers) == 1:
-            raise ValueError, 'Could not load any layer.'
+            # no assertion (allow empty documents)
+            print 'Warning: Could not load any layer, document is empty.'
 
         if no_background:
             # recognize solid or tiled background layers, at least those that mypaint <= 0.7.1 saves
             t1 = time.time()
             p = last_pixbuf
             if not p.get_has_alpha() and p.get_width() % N == 0 and p.get_height() % N == 0:
-                tiles = self.layers[0].surface.tiledict.values()
+                tiles = self.layers[0]._surface.tiledict.values()
                 if len(tiles) > 1:
                     all_equal = True
                     for tile in tiles[1:]: