layers: save+load locked+selected states in ORA
[mypaint:achadwick-mypaint.git] / lib / document.py
1 # This file is part of MyPaint.
2 # Copyright (C) 2007-2008 by Martin Renold <martinxyz@gmx.ch>
3 #
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 2 of the License, or
7 # (at your option) any later version.
8
9 import os, zipfile, tempfile, time, traceback
10 join = os.path.join
11 from cStringIO import StringIO
12 import xml.etree.ElementTree as ET
13 from gtk import gdk
14 import gobject, numpy
15 from gettext import gettext as _
16
17 import helpers, tiledsurface, pixbufsurface, backgroundsurface, mypaintlib
18 import command, stroke, layer
19 import brush
20
21 N = tiledsurface.N
22 LOAD_CHUNK_SIZE = 64*1024
23
24 from layer import DEFAULT_COMPOSITE_OP, VALID_COMPOSITE_OPS
25
26 class SaveLoadError(Exception):
27     """Expected errors on loading or saving, like missing permissions or non-existing files."""
28     pass
29
30 class Document():
31     """
32     This is the "model" in the Model-View-Controller design.
33     (The "view" would be ../gui/tileddrawwidget.py.)
34     It represents everything that the user would want to save.
35
36
37     The "controller" mostly in drawwindow.py.
38     It is possible to use it without any GUI attached (see ../tests/)
39     """
40     # Please note the following difficulty with the undo stack:
41     #
42     #   Most of the time there is an unfinished (but already rendered)
43     #   stroke pending, which has to be turned into a command.Action
44     #   or discarded as empty before any other action is possible.
45     #   (split_stroke)
46
47     def __init__(self, brushinfo=None):
48         if not brushinfo:
49             brushinfo = brush.BrushInfo()
50             brushinfo.load_defaults()
51         self.brush = brush.Brush(brushinfo)
52         self.brush.brushinfo.observers.append(self.brushsettings_changed_cb)
53         self.stroke = None
54         self.canvas_observers = []
55         self.stroke_observers = [] # callback arguments: stroke, brush (brush is a temporary read-only convenience object)
56         self.doc_observers = []
57         self.frame_observers = []
58         self.command_stack_observers = []
59         self.clear(True)
60
61         self._frame = [0, 0, 0, 0]
62         self._frame_enabled = False
63         # Used by move_frame() to accumulate values
64         self._frame_dx = 0.0
65         self._frame_dy = 0.0
66
67     def move_current_layer(self, dx, dy):
68         layer = self.layers[self.layer_idx]
69         layer.translate(dx, dy)
70
71     def get_frame(self):
72         return self._frame
73
74     def move_frame(self, dx=0.0, dy=0.0):
75         """Move the frame. Accumulates changes and moves the frame once
76         the accumulated change reaches the minimum move step."""
77         # FIXME: Should be 1 (pixel aligned), not tile aligned
78         # This is due to PNG saving having to be tile aligned
79         min_step = N
80
81         def round_to_n(value, n):
82             return int(round(value/n)*n)
83
84         x, y, w, h = self.get_frame()
85
86         self._frame_dx += dx
87         self._frame_dy += dy
88         step_x = round_to_n(self._frame_dx, min_step)
89         step_y = round_to_n(self._frame_dy, min_step)
90
91         if step_x:
92             self.set_frame(x=x+step_x)
93             self._frame_dx -= step_x
94
95         if step_y:
96             self.set_frame(y=y+step_y)
97             self._frame_dy -= step_y
98
99     def set_frame(self, x=None, y=None, width=None, height=None):
100         """Set the size of the frame. Pass None to indicate no-change."""
101
102         for i, var in enumerate([x, y, width, height]):
103             if not var is None:
104                 # FIXME: must be aligned to tile size due to PNG saving
105                 assert not var % N, "Frame size must be aligned to tile size"
106                 self._frame[i] = var
107
108         for f in self.frame_observers: f()
109
110     def get_frame_enabled(self):
111         return self._frame_enabled
112
113     def set_frame_enabled(self, enabled):
114         self._frame_enabled = enabled
115         for f in self.frame_observers: f()
116     frame_enabled = property(get_frame_enabled)
117
118     def call_doc_observers(self):
119         for f in self.doc_observers:
120             f(self)
121         return True
122
123     def clear(self, init=False):
124         self.split_stroke()
125         if not init:
126             bbox = self.get_bbox()
127         # throw everything away, including undo stack
128
129         self.command_stack = command.CommandStack()
130         self.command_stack.stack_observers = self.command_stack_observers
131         self.set_background((255, 255, 255))
132         self.layers = []
133         self.layer_idx = None
134         self.add_layer(0)
135         # disallow undo of the first layer
136         self.command_stack.clear()
137         self.unsaved_painting_time = 0.0
138
139         if not init:
140             for f in self.canvas_observers:
141                 f(*bbox)
142
143         self.call_doc_observers()
144
145     def get_current_layer(self):
146         return self.layers[self.layer_idx]
147     layer = property(get_current_layer)
148
149     def split_stroke(self):
150         if not self.stroke: return
151         self.stroke.stop_recording()
152         if not self.stroke.empty:
153             self.command_stack.do(command.Stroke(self, self.stroke, self.snapshot_before_stroke))
154             del self.snapshot_before_stroke
155             self.unsaved_painting_time += self.stroke.total_painting_time
156             for f in self.stroke_observers:
157                 f(self.stroke, self.brush)
158         self.stroke = None
159
160     def brushsettings_changed_cb(self, settings):
161         # The brush settings below are expected to change often in
162         # mid-stroke eg. by heavy keyboard usage. If only those
163         # change, we don't create a new undo step. (And thus als no
164         # separate pickable stroke in the strokemap.)
165         leightweight_brushsettings = set((
166             'radius_logarithmic', 'color_h', 'color_s', 'color_v',
167             'opaque', 'hardness', 'slow_tracking', 'slow_tracking_per_dab'
168             ))
169         if settings - leightweight_brushsettings:
170             self.split_stroke()
171
172     def select_layer(self, idx):
173         self.do(command.SelectLayer(self, idx))
174
175     def record_layer_move(self, layer, dx, dy):
176         layer_idx = self.layers.index(layer)
177         self.do(command.MoveLayer(self, layer_idx, dx, dy, True))
178
179     def move_layer(self, was_idx, new_idx, select_new=False):
180         self.do(command.ReorderSingleLayer(self, was_idx, new_idx, select_new))
181
182     def duplicate_layer(self, insert_idx=None, name=''):
183         self.do(command.DuplicateLayer(self, insert_idx, name))
184
185     def reorder_layers(self, new_layers):
186         self.do(command.ReorderLayers(self, new_layers))
187
188     def clear_layer(self):
189         if not self.layer.is_empty():
190             self.do(command.ClearLayer(self))
191
192     def stroke_to(self, dtime, x, y, pressure, xtilt, ytilt):
193         if not self.stroke:
194             self.stroke = stroke.Stroke()
195             self.stroke.start_recording(self.brush)
196             self.snapshot_before_stroke = self.layer.save_snapshot()
197         self.stroke.record_event(dtime, x, y, pressure, xtilt, ytilt)
198
199         split = self.layer.stroke_to(self.brush, x, y,
200                                 pressure, xtilt, ytilt, dtime)
201
202         if split:
203             self.split_stroke()
204
205     def redo_last_stroke_with_different_brush(self, brush):
206         cmd = self.get_last_command()
207         if not isinstance(cmd, command.Stroke):
208             return
209         cmd = self.undo()
210         assert isinstance(cmd, command.Stroke)
211         new_stroke = cmd.stroke.copy_using_different_brush(brush)
212         snapshot_before = self.layer.save_snapshot()
213         new_stroke.render(self.layer._surface)
214         self.do(command.Stroke(self, new_stroke, snapshot_before))
215
216     def layer_modified_cb(self, *args):
217         # for now, any layer modification is assumed to be visible
218         for f in self.canvas_observers:
219             f(*args)
220
221     def invalidate_all(self):
222         for f in self.canvas_observers:
223             f(0, 0, 0, 0)
224
225     def undo(self):
226         self.split_stroke()
227         while 1:
228             cmd = self.command_stack.undo()
229             if not cmd or not cmd.automatic_undo:
230                 return cmd
231
232     def redo(self):
233         self.split_stroke()
234         while 1:
235             cmd = self.command_stack.redo()
236             if not cmd or not cmd.automatic_undo:
237                 return cmd
238
239     def do(self, cmd):
240         self.split_stroke()
241         self.command_stack.do(cmd)
242
243     def get_last_command(self):
244         self.split_stroke()
245         return self.command_stack.get_last_command()
246
247     def get_bbox(self):
248         res = helpers.Rect()
249         for layer in self.layers:
250             # OPTIMIZE: only visible layers...
251             # careful: currently saving assumes that all layers are included
252             bbox = layer.get_bbox()
253             res.expandToIncludeRect(bbox)
254         return res
255
256     def get_effective_bbox(self):
257         """Return the effective bounding box of the document.
258         If the frame is enabled, this is the bounding box of the frame, 
259         else the (dynamic) bounding box of the document."""
260         return self.get_frame() if self.frame_enabled else self.get_bbox()
261
262     def blit_tile_into(self, dst_8bit, dst_has_alpha, tx, ty, mipmap_level=0, layers=None, background=None):
263         assert dst_has_alpha is False
264         if layers is None:
265             layers = self.layers
266         if background is None:
267             background = self.background
268
269         assert dst_8bit.dtype == 'uint8'
270         assert dst_8bit.shape[-1] == 4
271         dst = numpy.empty((N, N, 4), dtype='uint16')
272
273         background.blit_tile_into(dst, dst_has_alpha, tx, ty, mipmap_level)
274
275         for layer in layers:
276             surface = layer._surface
277             surface.composite_tile(dst, dst_has_alpha, tx, ty,
278                     mipmap_level=mipmap_level,
279                     opacity=layer.effective_opacity,
280                     mode=layer.compositeop)
281
282         mypaintlib.tile_convert_rgbu16_to_rgbu8(dst, dst_8bit)
283
284     def add_layer(self, insert_idx=None, after=None, name=''):
285         self.do(command.AddLayer(self, insert_idx, after, name))
286
287     def remove_layer(self,layer=None):
288         if len(self.layers) > 1:
289             self.do(command.RemoveLayer(self,layer))
290         else:
291             self.clear_layer()
292
293     def merge_layer_down(self):
294         dst_idx = self.layer_idx - 1
295         if dst_idx < 0:
296             return False
297         self.do(command.MergeLayer(self, dst_idx))
298         return True
299
300     def load_layer_from_pixbuf(self, pixbuf, x=0, y=0):
301         arr = helpers.gdkpixbuf2numpy(pixbuf)
302         s = tiledsurface.Surface()
303         s.load_from_numpy(arr, x, y)
304         self.do(command.LoadLayer(self, s))
305
306     def load_layer_from_png(self, filename, x=0, y=0, feedback_cb=None):
307         s = tiledsurface.Surface()
308         s.load_from_png(filename, x, y, feedback_cb)
309         self.do(command.LoadLayer(self, s))
310
311     def set_layer_visibility(self, visible, layer):
312         cmd = self.get_last_command()
313         if isinstance(cmd, command.SetLayerVisibility) and cmd.layer is layer:
314             self.undo()
315         self.do(command.SetLayerVisibility(self, visible, layer))
316
317     def set_layer_locked(self, locked, layer):
318         cmd = self.get_last_command()
319         if isinstance(cmd, command.SetLayerLocked) and cmd.layer is layer:
320             self.undo()
321         self.do(command.SetLayerLocked(self, locked, layer))
322
323     def set_layer_opacity(self, opacity, layer=None):
324         """Sets the opacity of a layer. If layer=None, works on the current layer"""
325         cmd = self.get_last_command()
326         if isinstance(cmd, command.SetLayerOpacity):
327             self.undo()
328         self.do(command.SetLayerOpacity(self, opacity, layer))
329
330     def set_layer_compositeop(self, compositeop, layer=None):
331         """Sets the composition-operation of a layer. If layer=None, works on the current layer"""
332         if compositeop not in VALID_COMPOSITE_OPS:
333             compositeop = DEFAULT_COMPOSITE_OP
334         cmd = self.get_last_command()
335         if isinstance(cmd, command.SetLayerCompositeOp):
336             self.undo()
337         self.do(command.SetLayerCompositeOp(self, compositeop, layer))
338
339     def set_background(self, obj):
340         # This is not an undoable action. One reason is that dragging
341         # on the color chooser would get tons of undo steps.
342
343         if not isinstance(obj, backgroundsurface.Background):
344             obj = backgroundsurface.Background(obj)
345         self.background = obj
346
347         self.invalidate_all()
348
349     def load_from_pixbuf(self, pixbuf):
350         """Load a document from a pixbuf."""
351         self.clear()
352         self.load_layer_from_pixbuf(pixbuf)
353         self.set_frame(*self.get_bbox())
354
355     def is_layered(self):
356         count = 0
357         for l in self.layers:
358             if not l.is_empty():
359                 count += 1
360         return count > 1
361
362     def is_empty(self):
363         return len(self.layers) == 1 and self.layer.is_empty()
364
365     def save(self, filename, **kwargs):
366         self.split_stroke()
367         junk, ext = os.path.splitext(filename)
368         ext = ext.lower().replace('.', '')
369         save = getattr(self, 'save_' + ext, self.unsupported)
370         try:
371             save(filename, **kwargs)
372         except gobject.GError, e:
373             traceback.print_exc()
374             if e.code == 5:
375                 #add a hint due to a very consfusing error message when there is no space left on device
376                 raise SaveLoadError, _('Unable to save: %s\nDo you have enough space left on the device?') % e.message
377             else:
378                 raise SaveLoadError, _('Unable to save: %s') % e.message
379         except IOError, e:
380             traceback.print_exc()
381             raise SaveLoadError, _('Unable to save: %s') % e.strerror
382         self.unsaved_painting_time = 0.0
383
384     def load(self, filename, **kwargs):
385         if not os.path.isfile(filename):
386             raise SaveLoadError, _('File does not exist: %s') % repr(filename)
387         if not os.access(filename,os.R_OK):
388             raise SaveLoadError, _('You do not have the necessary permissions to open file: %s') % repr(filename)
389         junk, ext = os.path.splitext(filename)
390         ext = ext.lower().replace('.', '')
391         load = getattr(self, 'load_' + ext, self.unsupported)
392         try:
393             load(filename, **kwargs)
394         except gobject.GError, e:
395             traceback.print_exc()
396             raise SaveLoadError, _('Error while loading: GError %s') % e
397         except IOError, e:
398             traceback.print_exc()
399             raise SaveLoadError, _('Error while loading: IOError %s') % e
400         self.command_stack.clear()
401         self.unsaved_painting_time = 0.0
402         self.call_doc_observers()
403
404     def unsupported(self, filename, *args, **kwargs):
405         raise SaveLoadError, _('Unknown file format extension: %s') % repr(filename)
406
407     def render_as_pixbuf(self, *args, **kwargs):
408         return pixbufsurface.render_as_pixbuf(self, *args, **kwargs)
409
410     def render_thumbnail(self):
411         t0 = time.time()
412         x, y, w, h = self.get_effective_bbox()
413         if w == 0 or h == 0:
414             # workaround to save empty documents
415             x, y, w, h = 0, 0, tiledsurface.N, tiledsurface.N
416         mipmap_level = 0
417         while mipmap_level < tiledsurface.MAX_MIPMAP_LEVEL and max(w, h) >= 512:
418             mipmap_level += 1
419             x, y, w, h = x/2, y/2, w/2, h/2
420
421         pixbuf = self.render_as_pixbuf(x, y, w, h, mipmap_level=mipmap_level)
422         assert pixbuf.get_width() == w and pixbuf.get_height() == h
423         pixbuf = helpers.scale_proportionally(pixbuf, 256, 256)
424         print 'Rendered thumbnail in', time.time() - t0, 'seconds.'
425         return pixbuf
426
427     def save_png(self, filename, alpha=False, multifile=False, **kwargs):
428         doc_bbox = self.get_effective_bbox()
429         if multifile:
430             self.save_multifile_png(filename, **kwargs)
431         else:
432             if alpha:
433                 tmp_layer = layer.Layer()
434                 for l in self.layers:
435                     l.merge_into(tmp_layer)
436                 tmp_layer.save_as_png(filename, *doc_bbox)
437             else:
438                 pixbufsurface.save_as_png(self, filename, *doc_bbox, alpha=False, **kwargs)
439
440     def save_multifile_png(self, filename, alpha=False, **kwargs):
441         prefix, ext = os.path.splitext(filename)
442         # if we have a number already, strip it
443         l = prefix.rsplit('.', 1)
444         if l[-1].isdigit():
445             prefix = l[0]
446         doc_bbox = self.get_effective_bbox()
447         for i, l in enumerate(self.layers):
448             filename = '%s.%03d%s' % (prefix, i+1, ext)
449             l.save_as_png(filename, *doc_bbox, **kwargs)
450
451     def load_png(self, filename, feedback_cb=None):
452         self.clear()
453         self.load_layer_from_png(filename, 0, 0, feedback_cb)
454         self.set_frame(*self.get_bbox())
455
456     @staticmethod
457     def _pixbuf_from_stream(fp, feedback_cb=None):
458         loader = gdk.PixbufLoader()
459         while True:
460             if feedback_cb is not None:
461                 feedback_cb()
462             buf = fp.read(LOAD_CHUNK_SIZE)
463             if buf == '':
464                 break
465             loader.write(buf)
466         loader.close()
467         return loader.get_pixbuf()
468
469     def load_from_pixbuf_file(self, filename, feedback_cb=None):
470         fp = open(filename, 'rb')
471         pixbuf = self._pixbuf_from_stream(fp, feedback_cb)
472         fp.close()
473         self.load_from_pixbuf(pixbuf)
474
475     load_jpg = load_from_pixbuf_file
476     load_jpeg = load_from_pixbuf_file
477
478     def save_jpg(self, filename, quality=90, **kwargs):
479         x, y, w, h = self.get_effective_bbox()
480         if w == 0 or h == 0:
481             x, y, w, h = 0, 0, N, N # allow to save empty documents
482         pixbuf = self.render_as_pixbuf(x, y, w, h, **kwargs)
483         pixbuf.save(filename, 'jpeg', options={'quality':str(quality)})
484
485     save_jpeg = save_jpg
486
487     def save_ora(self, filename, options=None, **kwargs):
488         print 'save_ora:'
489         t0 = time.time()
490         tempdir = tempfile.mkdtemp(u'mypaint')
491         # use .tmp extension, so we don't overwrite a valid file if there is an exception
492         z = zipfile.ZipFile(filename + '.tmpsave', 'w', compression=zipfile.ZIP_STORED)
493         # work around a permission bug in the zipfile library: http://bugs.python.org/issue3394
494         def write_file_str(filename, data):
495             zi = zipfile.ZipInfo(filename)
496             zi.external_attr = 0100644 << 16
497             z.writestr(zi, data)
498         write_file_str('mimetype', 'image/openraster') # must be the first file
499         image = ET.Element('image')
500         stack = ET.SubElement(image, 'stack')
501         x0, y0, w0, h0 = self.get_effective_bbox()
502         a = image.attrib
503         a['w'] = str(w0)
504         a['h'] = str(h0)
505
506         def store_pixbuf(pixbuf, name):
507             tmp = join(tempdir, 'tmp.png')
508             t1 = time.time()
509             pixbuf.save(tmp, 'png')
510             print '  %.3fs pixbuf saving %s' % (time.time() - t1, name)
511             z.write(tmp, name)
512             os.remove(tmp)
513
514         def store_surface(surface, name, rect=[]):
515             tmp = join(tempdir, 'tmp.png')
516             t1 = time.time()
517             surface.save_as_png(tmp, *rect, **kwargs)
518             print '  %.3fs surface saving %s' % (time.time() - t1, name)
519             z.write(tmp, name)
520             os.remove(tmp)
521
522         def add_layer(x, y, opac, surface, name, layer_name, visible=True,
523                       locked=False, selected=False,
524                       compositeop=DEFAULT_COMPOSITE_OP, rect=[]):
525             layer = ET.Element('layer')
526             stack.append(layer)
527             store_surface(surface, name, rect)
528             a = layer.attrib
529             if layer_name:
530                 a['name'] = layer_name
531             a['src'] = name
532             a['x'] = str(x)
533             a['y'] = str(y)
534             a['opacity'] = str(opac)
535             if compositeop not in VALID_COMPOSITE_OPS:
536                 compositeop = DEFAULT_COMPOSITE_OP
537             a['composite-op'] = compositeop
538             if visible:
539                 a['visibility'] = 'visible'
540             else:
541                 a['visibility'] = 'hidden'
542             if locked:
543                 a['edit-locked'] = 'true'
544             if selected:
545                 a['selected'] = 'true'
546             return layer
547
548         for idx, l in enumerate(reversed(self.layers)):
549             if l.is_empty():
550                 continue
551             opac = l.opacity
552             x, y, w, h = l.get_bbox()
553             sel = (idx == self.layer_idx)
554             el = add_layer(x-x0, y-y0, opac, l._surface,
555                            'data/layer%03d.png' % idx, l.name, l.visible,
556                            locked=l.locked, selected=sel,
557                            compositeop=l.compositeop, rect=(x, y, w, h))
558             # strokemap
559             sio = StringIO()
560             l.save_strokemap_to_file(sio, -x, -y)
561             data = sio.getvalue(); sio.close()
562             name = 'data/layer%03d_strokemap.dat' % idx
563             el.attrib['mypaint_strokemap_v2'] = name
564             write_file_str(name, data)
565
566         # save background as layer (solid color or tiled)
567         bg = self.background
568         # save as fully rendered layer
569         x, y, w, h = self.get_bbox()
570         l = add_layer(x-x0, y-y0, 1.0, bg, 'data/background.png', 'background',
571                       locked=True, selected=False,
572                       compositeop=DEFAULT_COMPOSITE_OP,
573                       rect=(x,y,w,h))
574         x, y, w, h = bg.get_pattern_bbox()
575         # save as single pattern (with corrected origin)
576         store_surface(bg, 'data/background_tile.png', rect=(x+x0, y+y0, w, h))
577         l.attrib['background_tile'] = 'data/background_tile.png'
578
579         # preview (256x256)
580         t2 = time.time()
581         print '  starting to render full image for thumbnail...'
582
583         thumbnail_pixbuf = self.render_thumbnail()
584         store_pixbuf(thumbnail_pixbuf, 'Thumbnails/thumbnail.png')
585         print '  total %.3fs spent on thumbnail' % (time.time() - t2)
586
587         helpers.indent_etree(image)
588         xml = ET.tostring(image, encoding='UTF-8')
589
590         write_file_str('stack.xml', xml)
591         z.close()
592         os.rmdir(tempdir)
593         if os.path.exists(filename):
594             os.remove(filename) # windows needs that
595         os.rename(filename + '.tmpsave', filename)
596
597         print '%.3fs save_ora total' % (time.time() - t0)
598
599         return thumbnail_pixbuf
600
601     @staticmethod
602     def __xsd2bool(v):
603         v = str(v).lower()
604         if v in ['true', '0']: return True
605         else: return False
606
607     def load_ora(self, filename, feedback_cb=None):
608         """Loads from an OpenRaster file"""
609         print 'load_ora:'
610         t0 = time.time()
611         tempdir = tempfile.mkdtemp(u'mypaint')
612         z = zipfile.ZipFile(filename)
613         print 'mimetype:', z.read('mimetype').strip()
614         xml = z.read('stack.xml')
615         image = ET.fromstring(xml)
616         stack = image.find('stack')
617
618         w = int(image.attrib['w'])
619         h = int(image.attrib['h'])
620
621         def round_up_to_n(value, n):
622             assert value >= 0, "function undefined for negative numbers"
623
624             residual = value % n
625             if residual:
626                 value = value - residual + n
627             return int(value)
628
629         def get_pixbuf(filename):
630             t1 = time.time()
631
632             try:
633                 fp = z.open(filename, mode='r')
634             except KeyError:
635                 # support for bad zip files (saved by old versions of the GIMP ORA plugin)
636                 fp = z.open(filename.encode('utf-8'), mode='r')
637                 print 'WARNING: bad OpenRaster ZIP file. There is an utf-8 encoded filename that does not have the utf-8 flag set:', repr(filename)
638
639             res = self._pixbuf_from_stream(fp, feedback_cb)
640             fp.close()
641             print '  %.3fs loading %s' % (time.time() - t1, filename)
642             return res
643
644         def get_layers_list(root, x=0,y=0):
645             res = []
646             for item in root:
647                 if item.tag == 'layer':
648                     if 'x' in item.attrib:
649                         item.attrib['x'] = int(item.attrib['x']) + x
650                     if 'y' in item.attrib:
651                         item.attrib['y'] = int(item.attrib['y']) + y
652                     res.append(item)
653                 elif item.tag == 'stack':
654                     stack_x = int( item.attrib.get('x', 0) )
655                     stack_y = int( item.attrib.get('y', 0) )
656                     res += get_layers_list(item, stack_x, stack_y)
657                 else:
658                     print 'Warning: ignoring unsupported tag:', item.tag
659             return res
660
661         self.clear() # this leaves one empty layer
662         no_background = True
663         # FIXME: don't require tile alignment for frame
664         self.set_frame(width=round_up_to_n(w, N), height=round_up_to_n(h, N))
665
666         selected_layer = None
667         for layer in get_layers_list(stack):
668             a = layer.attrib
669
670             if 'background_tile' in a:
671                 assert no_background
672                 try:
673                     print a['background_tile']
674                     self.set_background(get_pixbuf(a['background_tile']))
675                     no_background = False
676                     continue
677                 except backgroundsurface.BackgroundError, e:
678                     print 'ORA background tile not usable:', e
679
680             src = a.get('src', '')
681             if not src.lower().endswith('.png'):
682                 print 'Warning: ignoring non-png layer'
683                 continue
684             name = a.get('name', '')
685             x = int(a.get('x', '0'))
686             y = int(a.get('y', '0'))
687             opac = float(a.get('opacity', '1.0'))
688             compositeop = str(a.get('composite-op', DEFAULT_COMPOSITE_OP))
689             if compositeop not in VALID_COMPOSITE_OPS:
690                 compositeop = DEFAULT_COMPOSITE_OP
691             selected = self.__xsd2bool(a.get("selected", 'false'))
692             locked = self.__xsd2bool(a.get("edit-locked", 'false'))
693
694             visible = not 'hidden' in a.get('visibility', 'visible')
695             self.add_layer(insert_idx=0, name=name)
696             t1 = time.time()
697
698             # extract the png form the zip into a file first
699             # the overhead for doing so seems to be neglegible (around 5%)
700             z.extract(src, tempdir)
701             tmp_filename = join(tempdir, src)
702             self.load_layer_from_png(tmp_filename, x, y, feedback_cb)
703             os.remove(tmp_filename)
704
705             layer = self.layers[0]
706
707             self.set_layer_opacity(helpers.clamp(opac, 0.0, 1.0), layer)
708             self.set_layer_compositeop(compositeop, layer)
709             self.set_layer_visibility(visible, layer)
710             self.set_layer_locked(locked, layer)
711             if selected:
712                 selected_layer = layer
713             print '  %.3fs loading and converting layer png' % (time.time() - t1)
714             # strokemap
715             fname = a.get('mypaint_strokemap_v2', None)
716             if fname:
717                 if x % N or y % N:
718                     print 'Warning: dropping non-aligned strokemap'
719                 else:
720                     sio = StringIO(z.read(fname))
721                     layer.load_strokemap_from_file(sio, x, y)
722                     sio.close()
723
724         if len(self.layers) == 1:
725             # no assertion (allow empty documents)
726             print 'Warning: Could not load any layer, document is empty.'
727
728         if len(self.layers) > 1:
729             # remove the still present initial empty top layer
730             self.select_layer(len(self.layers)-1)
731             self.remove_layer()
732             # this leaves the topmost layer selected
733
734         if selected_layer is not None:
735             for i, layer in zip(range(len(self.layers)), self.layers):
736                 if layer is selected_layer:
737                     self.select_layer(i)
738                     break
739
740         z.close()
741
742         # remove empty directories created by zipfile's extract()
743         for root, dirs, files in os.walk(tempdir, topdown=False):
744             for name in dirs:
745                 os.rmdir(os.path.join(root, name))
746         os.rmdir(tempdir)
747
748         print '%.3fs load_ora total' % (time.time() - t0)