OSDN Git Service

layer lock: prevent accidental selection & drawing
[mypaint-anime/master.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 N = tiledsurface.N
21
22 class SaveLoadError(Exception):
23     """Expected errors on loading or saving, like missing permissions or non-existing files."""
24     pass
25
26 class Document():
27     """
28     This is the "model" in the Model-View-Controller design.
29     (The "view" would be ../gui/tileddrawwidget.py.)
30     It represents everything that the user would want to save.
31
32
33     The "controller" mostly in drawwindow.py.
34     It is possible to use it without any GUI attached (see ../tests/)
35     """
36     # Please note the following difficulty with the undo stack:
37     #
38     #   Most of the time there is an unfinished (but already rendered)
39     #   stroke pending, which has to be turned into a command.Action
40     #   or discarded as empty before any other action is possible.
41     #   (split_stroke)
42
43     def __init__(self):
44         self.brush = brush.Brush()
45         self.stroke = None
46         self.canvas_observers = []
47         self.stroke_observers = [] # callback arguments: stroke, brush (brush is a temporary read-only convenience object)
48         self.doc_observers = []
49         self.frame_observers = []
50         self.clear(True)
51
52         self._frame = [0, 0, 0, 0]
53         self._frame_enabled = False
54         # Used by move_frame() to accumulate values
55         self._frame_dx = 0.0
56         self._frame_dy = 0.0
57
58     def get_frame(self):
59         return self._frame
60
61     def move_frame(self, dx=0.0, dy=0.0):
62         """Move the frame. Accumulates changes and moves the frame once
63         the accumulated change reaches the minimum move step."""
64         # FIXME: Should be 1 (pixel aligned), not tile aligned
65         # This is due to PNG saving having to be tile aligned
66         min_step = N
67
68         def round_to_n(value, n):
69             return int(round(value/n)*n)
70
71         x, y, w, h = self.get_frame()
72
73         self._frame_dx += dx
74         self._frame_dy += dy
75         step_x = round_to_n(self._frame_dx, min_step)
76         step_y = round_to_n(self._frame_dy, min_step)
77
78         if step_x:
79             self.set_frame(x=x+step_x)
80             self._frame_dx -= step_x
81
82         if step_y:
83             self.set_frame(y=y+step_y)
84             self._frame_dy -= step_y
85
86     def set_frame(self, x=None, y=None, width=None, height=None):
87         """Set the size of the frame. Pass None to indicate no-change."""
88
89         for i, var in enumerate([x, y, width, height]):
90             if not var is None:
91                 # FIXME: must be aligned to tile size due to PNG saving
92                 assert not var % N, "Frame size must be aligned to tile size"
93                 self._frame[i] = var
94
95         for f in self.frame_observers: f()
96
97     def get_frame_enabled(self):
98         return self._frame_enabled
99
100     def set_frame_enabled(self, enabled):
101         self._frame_enabled = enabled
102         for f in self.frame_observers: f()
103     frame_enabled = property(get_frame_enabled)
104
105     def call_doc_observers(self):
106         for f in self.doc_observers:
107             f(self)
108         return True
109
110     def clear(self, init=False):
111         self.split_stroke()
112         if not init:
113             bbox = self.get_bbox()
114         # throw everything away, including undo stack
115         self.command_stack = command.CommandStack()
116         self.set_background((255, 255, 255))
117         self.layers = []
118         self.layer_idx = None
119         self.add_layer(0)
120         # disallow undo of the first layer
121         self.command_stack.clear()
122         self.unsaved_painting_time = 0.0
123
124         if not init:
125             for f in self.canvas_observers:
126                 f(*bbox)
127
128         self.call_doc_observers()
129
130     def get_current_layer(self):
131         return self.layers[self.layer_idx]
132     layer = property(get_current_layer)
133
134     def split_stroke(self):
135         if not self.stroke: return
136         self.stroke.stop_recording()
137         if not self.stroke.empty:
138             self.command_stack.do(command.Stroke(self, self.stroke, self.snapshot_before_stroke))
139             del self.snapshot_before_stroke
140             self.unsaved_painting_time += self.stroke.total_painting_time
141             for f in self.stroke_observers:
142                 f(self.stroke, self.brush)
143         self.stroke = None
144
145     def select_layer(self, idx):
146         self.do(command.SelectLayer(self, idx))
147
148     def move_layer(self, was_idx, new_idx):
149         self.do(command.MoveLayer(self, was_idx, new_idx))
150
151     def clear_layer(self):
152         if not self.layer.surface.is_empty():
153             self.do(command.ClearLayer(self))
154
155     def stroke_to(self, dtime, x, y, pressure, xtilt,ytilt):
156         if not self.stroke:
157             self.stroke = stroke.Stroke()
158             self.stroke.start_recording(self.brush)
159             self.snapshot_before_stroke = self.layer.save_snapshot()
160         self.stroke.record_event(dtime, x, y, pressure, xtilt,ytilt)
161
162         l = self.layer
163         l.surface.begin_atomic()
164         split = self.brush.stroke_to (l.surface, x, y, pressure, xtilt,ytilt, dtime)
165         l.surface.end_atomic()
166
167         if split:
168             self.split_stroke()
169
170     def straight_line(self, src, dst):
171         self.split_stroke()
172         # TODO: undo last stroke if it was very short... (but not at document level?)
173         real_brush = self.brush
174         self.brush = brush.Brush()
175         self.brush.copy_settings_from(real_brush)
176
177         duration = 3.0
178         pressure = 0.3
179         N = 1000
180         x = numpy.linspace(src[0], dst[0], N)
181         y = numpy.linspace(src[1], dst[1], N)
182         # rest the brush in src for a minute, to avoid interpolation
183         # from the upper left corner (states are zero) (FIXME: the
184         # brush should handle this on its own, maybe?)
185         self.stroke_to(60.0, x[0], y[0], 0.0, 0.0, 0.0)
186         for i in xrange(N):
187             self.stroke_to(duration/N, x[i], y[i], pressure, 0.0, 0.0)
188         self.split_stroke()
189         self.brush = real_brush
190
191
192     def layer_modified_cb(self, *args):
193         # for now, any layer modification is assumed to be visible
194         for f in self.canvas_observers:
195             f(*args)
196
197     def invalidate_all(self):
198         for f in self.canvas_observers:
199             f(0, 0, 0, 0)
200
201     def undo(self):
202         self.split_stroke()
203         while 1:
204             cmd = self.command_stack.undo()
205             if not cmd or not cmd.automatic_undo:
206                 return cmd
207
208     def redo(self):
209         self.split_stroke()
210         while 1:
211             cmd = self.command_stack.redo()
212             if not cmd or not cmd.automatic_undo:
213                 return cmd
214
215     def do(self, cmd):
216         self.split_stroke()
217         self.command_stack.do(cmd)
218
219     def get_last_command(self):
220         self.split_stroke()
221         return self.command_stack.get_last_command()
222
223     def set_brush(self, brush):
224         self.split_stroke()
225         self.brush.copy_settings_from(brush)
226
227     def get_bbox(self):
228         res = helpers.Rect()
229         for layer in self.layers:
230             # OPTIMIZE: only visible layers...
231             # careful: currently saving assumes that all layers are included
232             bbox = layer.surface.get_bbox()
233             res.expandToIncludeRect(bbox)
234         return res
235
236     def get_effective_bbox(self):
237         """Return the effective bounding box of the document.
238         If the frame is enabled, this is the bounding box of the frame, 
239         else the (dynamic) bounding box of the document."""
240         return self.get_frame() if self.frame_enabled else self.get_bbox()
241
242     def blit_tile_into(self, dst_8bit, tx, ty, mipmap_level=0, layers=None, background=None):
243         if layers is None:
244             layers = self.layers
245         if background is None:
246             background = self.background
247
248         assert dst_8bit.dtype == 'uint8'
249         dst = numpy.empty((N, N, 3), dtype='uint16')
250
251         background.blit_tile_into(dst, tx, ty, mipmap_level)
252
253         for layer in layers:
254             surface = layer.surface
255             surface.composite_tile_over(dst, tx, ty, mipmap_level=mipmap_level, opacity=layer.effective_opacity)
256
257         mypaintlib.tile_convert_rgb16_to_rgb8(dst, dst_8bit)
258
259     def add_layer(self, insert_idx=None, after=None, name=''):
260         self.do(command.AddLayer(self, insert_idx, after, name))
261
262     def remove_layer(self,layer=None):
263         if len(self.layers) > 1:
264             self.do(command.RemoveLayer(self,layer))
265         else:
266             self.clear_layer()
267
268     def merge_layer_down(self):
269         dst_idx = self.layer_idx - 1
270         if dst_idx < 0:
271             return False
272         self.do(command.MergeLayer(self, dst_idx))
273         return True
274
275     def load_layer_from_pixbuf(self, pixbuf, x=0, y=0):
276         arr = helpers.gdkpixbuf2numpy(pixbuf)
277         self.do(command.LoadLayer(self, arr, x, y))
278
279     def set_layer_visibility(self, visible, layer):
280         cmd = self.get_last_command()
281         if isinstance(cmd, command.SetLayerVisibility) and cmd.layer is layer:
282             self.undo()
283         self.do(command.SetLayerVisibility(self, visible, layer))
284
285     def set_layer_locked(self, locked, layer):
286         cmd = self.get_last_command()
287         if isinstance(cmd, command.SetLayerLocked) and cmd.layer is layer:
288             self.undo()
289         self.do(command.SetLayerLocked(self, locked, layer))
290
291     def set_layer_opacity(self, opacity, layer=None):
292         """Sets the opacity of a layer. If layer=None, works on the current layer"""
293         cmd = self.get_last_command()
294         if isinstance(cmd, command.SetLayerOpacity):
295             self.undo()
296         self.do(command.SetLayerOpacity(self, opacity, layer))
297
298     def set_background(self, obj):
299         # This is not an undoable action. One reason is that dragging
300         # on the color chooser would get tons of undo steps.
301
302         if not isinstance(obj, backgroundsurface.Background):
303             obj = backgroundsurface.Background(obj)
304         self.background = obj
305
306         self.invalidate_all()
307
308     def load_from_pixbuf(self, pixbuf):
309         """Load a document from a pixbuf."""
310         self.clear()
311         self.load_layer_from_pixbuf(pixbuf)
312         self.set_frame(*self.get_bbox())
313
314     def is_layered(self):
315         count = 0
316         for l in self.layers:
317             if not l.surface.is_empty():
318                 count += 1
319         return count > 1
320
321     def is_empty(self):
322         return len(self.layers) == 1 and self.layer.surface.is_empty()
323
324     def save(self, filename, **kwargs):
325         self.split_stroke()
326         trash, ext = os.path.splitext(filename)
327         ext = ext.lower().replace('.', '')
328         save = getattr(self, 'save_' + ext, self.unsupported)
329         try:        
330             save(filename, **kwargs)
331         except gobject.GError, e:
332             traceback.print_exc()
333             if e.code == 5:
334                 #add a hint due to a very consfusing error message when there is no space left on device
335                 raise SaveLoadError, _('Unable to save: %s\nDo you have enough space left on the device?') % e.message
336             else:
337                 raise SaveLoadError, _('Unable to save: %s') % e.message
338         except IOError, e:
339             traceback.print_exc()
340             raise SaveLoadError, _('Unable to save: %s') % e.strerror
341         self.unsaved_painting_time = 0.0
342
343     def load(self, filename):
344         if not os.path.isfile(filename):
345             raise SaveLoadError, _('File does not exist: %s') % repr(filename)
346         if not os.access(filename,os.R_OK):
347             raise SaveLoadError, _('You do not have the necessary permissions to open file: %s') % repr(filename)
348         trash, ext = os.path.splitext(filename)
349         ext = ext.lower().replace('.', '')
350         load = getattr(self, 'load_' + ext, self.unsupported)
351         try:
352             load(filename)
353         except gobject.GError, e:
354             traceback.print_exc()
355             raise SaveLoadError, _('Error while loading: GError %s') % e
356         except IOError, e:
357             traceback.print_exc()
358             raise SaveLoadError, _('Error while loading: IOError %s') % e
359         self.command_stack.clear()
360         self.unsaved_painting_time = 0.0
361         self.call_doc_observers()
362
363     def unsupported(self, filename):
364         raise SaveLoadError, _('Unknown file format extension: %s') % repr(filename)
365
366     def render_as_pixbuf(self, *args, **kwargs):
367         return pixbufsurface.render_as_pixbuf(self, *args, **kwargs)
368
369     def render_thumbnail(self):
370         t0 = time.time()
371         x, y, w, h = self.get_effective_bbox()
372         mipmap_level = 0
373         while mipmap_level < tiledsurface.MAX_MIPMAP_LEVEL and max(w, h) >= 512:
374             mipmap_level += 1
375             x, y, w, h = x/2, y/2, w/2, h/2
376
377         pixbuf = self.render_as_pixbuf(x, y, w, h, mipmap_level=mipmap_level)
378         assert pixbuf.get_width() == w and pixbuf.get_height() == h
379         pixbuf = helpers.scale_proportionally(pixbuf, 256, 256)
380         print 'Rendered thumbnail in', time.time() - t0, 'seconds.'
381         return pixbuf
382
383     def save_png(self, filename, alpha=False, multifile=False):
384         doc_bbox = self.get_effective_bbox()
385         if multifile:
386             self.save_multifile_png(filename)
387         else:
388             if alpha:
389                 tmp_layer = layer.Layer()
390                 for l in self.layers:
391                     l.merge_into(tmp_layer)
392                 tmp_layer.surface.save(filename, *doc_bbox)
393             else:
394                 pixbufsurface.save_as_png(self, filename, *doc_bbox, alpha=False)
395
396     def save_multifile_png(self, filename, alpha=False):
397         prefix, ext = os.path.splitext(filename)
398         # if we have a number already, strip it
399         l = prefix.rsplit('.', 1)
400         if l[-1].isdigit():
401             prefix = l[0]
402         doc_bbox = self.get_effective_bbox()
403         for i, l in enumerate(self.layers):
404             filename = '%s.%03d%s' % (prefix, i+1, ext)
405             l.surface.save(filename, *doc_bbox)
406
407     def load_png(self, filename):
408         self.load_from_pixbuf(gdk.pixbuf_new_from_file(filename))
409
410     def load_jpg(self, filename):
411         self.load_from_pixbuf(gdk.pixbuf_new_from_file(filename))
412     load_jpeg = load_jpg
413
414     def save_jpg(self, filename, quality=90):
415         doc_bbox = self.get_effective_bbox()
416         pixbuf = self.render_as_pixbuf(*doc_bbox)
417         pixbuf.save(filename, 'jpeg', options={'quality':str(quality)})
418     save_jpeg = save_jpg
419
420     def save_ora(self, filename, options=None):
421         print 'save_ora:'
422         t0 = time.time()
423         tempdir = tempfile.mkdtemp('mypaint')
424         # use .tmp extension, so we don't overwrite a valid file if there is an exception
425         z = zipfile.ZipFile(filename + '.tmpsave', 'w', compression=zipfile.ZIP_STORED)
426         # work around a permission bug in the zipfile library: http://bugs.python.org/issue3394
427         def write_file_str(filename, data):
428             zi = zipfile.ZipInfo(filename)
429             zi.external_attr = 0100644 << 16
430             z.writestr(zi, data)
431         write_file_str('mimetype', 'image/openraster') # must be the first file
432         image = ET.Element('image')
433         stack = ET.SubElement(image, 'stack')
434         x0, y0, w0, h0 = self.get_effective_bbox()
435         a = image.attrib
436         a['w'] = str(w0)
437         a['h'] = str(h0)
438
439         def store_pixbuf(pixbuf, name):
440             tmp = join(tempdir, 'tmp.png')
441             t1 = time.time()
442             pixbuf.save(tmp, 'png')
443             print '  %.3fs pixbuf saving %s' % (time.time() - t1, name)
444             z.write(tmp, name)
445             os.remove(tmp)
446
447         def store_surface(surface, name, rect=[]):
448             tmp = join(tempdir, 'tmp.png')
449             t1 = time.time()
450             surface.save(tmp, *rect)
451             print '  %.3fs surface saving %s' % (time.time() - t1, name)
452             z.write(tmp, name)
453             os.remove(tmp)
454
455         def add_layer(x, y, opac, surface, name, layer_name, visible=True, rect=[]):
456             layer = ET.Element('layer')
457             stack.append(layer)
458             store_surface(surface, name, rect)
459             a = layer.attrib
460             if layer_name:
461                 a['name'] = layer_name
462             a['src'] = name
463             a['x'] = str(x)
464             a['y'] = str(y)
465             a['opacity'] = str(opac)
466             if visible:
467                 a['visibility'] = 'visible'
468             else:
469                 a['visibility'] = 'hidden'
470             return layer
471
472         for idx, l in enumerate(reversed(self.layers)):
473             if l.surface.is_empty():
474                 continue
475             opac = l.opacity
476             x, y, w, h = l.surface.get_bbox()
477             el = add_layer(x-x0, y-y0, opac, l.surface, 'data/layer%03d.png' % idx, l.name, l.visible, rect=(x, y, w, h))
478             # strokemap
479             sio = StringIO()
480             l.save_strokemap_to_file(sio, -x, -y)
481             data = sio.getvalue(); sio.close()
482             name = 'data/layer%03d_strokemap.dat' % idx
483             el.attrib['mypaint_strokemap_v2'] = name
484             write_file_str(name, data)
485
486         # save background as layer (solid color or tiled)
487         bg = self.background
488         # save as fully rendered layer
489         x, y, w, h = self.get_bbox()
490         l = add_layer(x, y, 1.0, bg, 'data/background.png', 'background', rect=(x,y,w,h))
491         x, y, w, h = bg.get_pattern_bbox()
492         # save as single pattern (with corrected origin)
493         store_surface(bg, 'data/background_tile.png', rect=(x+x0, y+y0, w, h))
494         l.attrib['background_tile'] = 'data/background_tile.png'
495
496         # preview (256x256)
497         t2 = time.time()
498         print '  starting to render full image for thumbnail...'
499
500         thumbnail_pixbuf = self.render_thumbnail()
501         store_pixbuf(thumbnail_pixbuf, 'Thumbnails/thumbnail.png')
502         print '  total %.3fs spent on thumbnail' % (time.time() - t2)
503
504         helpers.indent_etree(image)
505         xml = ET.tostring(image, encoding='UTF-8')
506
507         write_file_str('stack.xml', xml)
508         z.close()
509         os.rmdir(tempdir)
510         if os.path.exists(filename):
511             os.remove(filename) # windows needs that
512         os.rename(filename + '.tmpsave', filename)
513
514         print '%.3fs save_ora total' % (time.time() - t0)
515
516         return thumbnail_pixbuf
517
518     def load_ora(self, filename):
519         """Loads from an OpenRaster file"""
520         print 'load_ora:'
521         t0 = time.time()
522         tempdir = tempfile.mkdtemp('mypaint')
523         z = zipfile.ZipFile(filename)
524         print 'mimetype:', z.read('mimetype').strip()
525         xml = z.read('stack.xml')
526         image = ET.fromstring(xml)
527         stack = image.find('stack')
528
529         w = int(image.attrib['w'])
530         h = int(image.attrib['h'])
531
532         def round_up_to_n(value, n):
533             assert value >= 0, "function undefined for negative numbers"
534
535             residual = value % n
536             if residual:
537                 value = value - residual + n
538             return int(value)
539
540         def get_pixbuf(filename):
541             t1 = time.time()
542             tmp = join(tempdir, 'tmp.png')
543             f = open(tmp, 'wb')
544
545             try:
546                 data = z.read(filename)
547             except KeyError:
548                 # support for bad zip files (saved by old versions of the GIMP ORA plugin)
549                 data = z.read(filename.encode('utf-8'))
550                 print 'WARNING: bad OpenRaster ZIP file. There is an utf-8 encoded filename that does not have the utf-8 flag set:', repr(filename)
551
552             f.write(data)
553             f.close()
554             res = gdk.pixbuf_new_from_file(tmp)
555             os.remove(tmp)
556             print '  %.3fs loading %s' % (time.time() - t1, filename)
557             return res
558
559         def get_layers_list(root, x=0,y=0):
560             res = []
561             for item in root:
562                 if item.tag == 'layer':
563                     if 'x' in item.attrib:
564                         item.attrib['x'] = int(item.attrib['x']) + x
565                     if 'y' in item.attrib:
566                         item.attrib['y'] = int(item.attrib['y']) + y
567                     res.append(item)
568                 elif item.tag == 'stack':
569                     stack_x = int( item.attrib.get('x', 0) )
570                     stack_y = int( item.attrib.get('y', 0) )
571                     res += get_layers_list(item, stack_x, stack_y)
572                 else:
573                     print 'Warning: ignoring unsupported tag:', item.tag
574             return res
575
576         self.clear() # this leaves one empty layer
577         no_background = True
578         # FIXME: don't require tile alignment for frame
579         self.set_frame(width=round_up_to_n(w, N), height=round_up_to_n(h, N))
580
581         for layer in get_layers_list(stack):
582             a = layer.attrib
583
584             if 'background_tile' in a:
585                 assert no_background
586                 try:
587                     print a['background_tile']
588                     self.set_background(get_pixbuf(a['background_tile']))
589                     no_background = False
590                     continue
591                 except backgroundsurface.BackgroundError, e:
592                     print 'ORA background tile not usable:', e
593
594             src = a.get('src', '')
595             if not src.lower().endswith('.png'):
596                 print 'Warning: ignoring non-png layer'
597                 continue
598             pixbuf = get_pixbuf(src)
599             name = a.get('name', '')
600             x = int(a.get('x', '0'))
601             y = int(a.get('y', '0'))
602             opac = float(a.get('opacity', '1.0'))
603             visible = not 'hidden' in a.get('visibility', 'visible')
604             self.add_layer(insert_idx=0, name=name)
605             last_pixbuf = pixbuf
606             t1 = time.time()
607             self.load_layer_from_pixbuf(pixbuf, x, y)
608             layer = self.layers[0]
609
610             self.set_layer_opacity(helpers.clamp(opac, 0.0, 1.0), layer)
611             self.set_layer_visibility(visible, layer)
612             print '  %.3fs converting pixbuf to layer format' % (time.time() - t1)
613             # strokemap
614             fname = a.get('mypaint_strokemap_v2', None)
615             if fname:
616                 if x % N or y % N:
617                     print 'Warning: dropping non-aligned strokemap'
618                 else:
619                     sio = StringIO(z.read(fname))
620                     layer.load_strokemap_from_file(sio, x, y)
621                     sio.close()
622
623         os.rmdir(tempdir)
624
625         if len(self.layers) == 1:
626             raise ValueError, 'Could not load any layer.'
627
628         if no_background:
629             # recognize solid or tiled background layers, at least those that mypaint <= 0.7.1 saves
630             t1 = time.time()
631             p = last_pixbuf
632             if not p.get_has_alpha() and p.get_width() % N == 0 and p.get_height() % N == 0:
633                 tiles = self.layers[0].surface.tiledict.values()
634                 if len(tiles) > 1:
635                     all_equal = True
636                     for tile in tiles[1:]:
637                         if (tile.rgba != tiles[0].rgba).any():
638                             all_equal = False
639                             break
640                     if all_equal:
641                         arr = helpers.gdkpixbuf2numpy(p)
642                         tile = arr[0:N,0:N,:]
643                         self.set_background(tile.copy())
644                         self.select_layer(0)
645                         self.remove_layer()
646             print '  %.3fs recognizing tiled background' % (time.time() - t1)
647
648         if len(self.layers) > 1:
649             # remove the still present initial empty top layer
650             self.select_layer(len(self.layers)-1)
651             self.remove_layer()
652             # this leaves the topmost layer selected
653
654         print '%.3fs load_ora total' % (time.time() - t0)